acdream/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
Erik dc722e70bd feat(O-T7): drop WB project references; complete extraction
End of Phase O extraction. Final cleanup:

- Dropped <ProjectReference> entries to WorldBuilder.Shared and
  Chorizite.OpenGLSDLBackend from both AcDream.App.csproj and
  AcDream.Core.csproj.
- Added Chorizite.Core NuGet PackageReference to AcDream.Core.csproj
  (needed by Core.Rendering.Wb.TextureHelpers for TextureFormat enum;
  previously transitive through the WB project ref).
- Added BCnEncoder.Net.ImageSharp (1.1.2) + SixLabors.ImageSharp (3.1.12)
  as direct PackageReferences to AcDream.App.csproj — previously transitive
  via Chorizite.OpenGLSDLBackend project; used directly by ObjectMeshManager.

Item A (BaseObjectRenderManager static fields):
- Inlined CurrentAtlas/CurrentVAO/CurrentIBO into a new RenderStateCache.cs
  static class (AcDream.App.Rendering.Wb namespace) — the 4 consumers
  (ManagedGLIndexBuffer, ManagedGLTexture, ManagedGLTextureArray, ParticleBatcher)
  all reference RenderStateCache.* instead of BaseObjectRenderManager.*.
- Dropped using Chorizite.OpenGLSDLBackend.Lib from all 4 consumers and from
  WbDrawDispatcher (which had it only as a dead import).

Item B (ActiveParticleEmitter.ObjectLandblock):
- ObjectLandblock? erased to object?; WorldBuilder.Shared.Models.ObjectId? erased
  to ulong? — both fields are stored but never read by any consumer in our codebase.
- Dropped both WB using directives from ActiveParticleEmitter.cs.

Item C (IDatReaderWriter / IDatDatabase):
- Verbatim copy of both interfaces into IDatReaderWriter.cs in
  AcDream.App.Rendering.Wb namespace — DatCollectionAdapter and ObjectMeshManager
  already live in that namespace, so no using changes needed.
- Dropped using WorldBuilder.Shared.Services from DatCollectionAdapter.cs and
  ObjectMeshManager.cs.

Additional extractions required by the reference drop:
- GeometryUtils.cs: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils
  (float-precision overloads only; Vector3d double-precision overloads omitted —
  ObjectMeshManager uses only the float versions).
- Dropped using WorldBuilder.Shared.Lib from ObjectMeshManager.cs.

WbMeshAdapter.cs cleanup (spec O-D12):
- Deleted _wbDats (DefaultDatReaderWriter) field + ctor init + Dispose call.
- Deleted the [indoor-upload] NULL_RESULT diagnostic block (lines ~205-262) —
  its Phase 2 cell-resolution investigation is complete; its _wbDats.ResolveId
  dependency goes with this commit.
- Deleted _pendingEnvCellRequests field + isPendingEnvCell tracking in Tick().
- Simplified Tick() to a clean drain loop.

Deleted SplitFormulaDivergenceTest.cs — one-time N.5b data-collection sweep;
job done.

Verified acceptance criteria:
- Zero <ProjectReference> to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* in any csproj.
- Zero 'using WorldBuilder.*' / 'using Chorizite.OpenGLSDLBackend.*' in src/.
- DefaultDatReaderWriter referenced in zero places in src/ (comments only).

Build green (0 warnings, 0 errors).
Tests: 1154 total (-1 from deleted SplitFormulaDivergenceTest), 1146 pass,
8 pre-existing failures (unchanged from baseline — physics/input tests
unrelated to this change).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:17:33 +02:00

227 lines
9 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using Chorizite.Core.Render;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
[StructLayout(LayoutKind.Sequential)]
public struct ParticleInstance {
public Vector3 Position;
public Vector3 ScaleOpacityActive; // x=scale, y=opacity, z=active (1.0 or 0.0)
public float TextureIndex;
public Quaternion Rotation;
public Vector2 Size;
public float IsBillboard; // 1.0 for true, 0.0 for false
}
public struct ParticleRenderData {
public ParticleInstance Instance;
public float DistanceSq;
public ManagedGLTextureArray? Atlas;
public bool IsAdditive;
}
public unsafe class ParticleBatcher : IDisposable {
private const int MAX_PARTICLES_TOTAL = 65536;
private readonly OpenGLGraphicsDevice _graphicsDevice;
private readonly uint _vao;
private readonly uint _vbo;
private readonly uint _ibo;
private readonly uint _instanceVbo;
private readonly IShader _shader;
private readonly ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL];
private readonly List<ParticleRenderData> _allParticles = new();
private int _currentInstanceCount = 0;
private ManagedGLTextureArray? _currentAtlas;
private bool _currentIsAdditive;
private Matrix4x4 _viewProjection;
private Vector3 _cameraUp;
private Vector3 _cameraRight;
public ParticleBatcher(OpenGLGraphicsDevice graphicsDevice) {
_graphicsDevice = graphicsDevice;
var gl = _graphicsDevice.GL;
var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
_shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
// Create quad vertices - centered to match ACViewer expansion logic
float[] vertices = {
// x, y, z, u, v
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f,
0.5f, 0.0f, -0.5f, 1.0f, 1.0f,
0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f
};
ushort[] indices = { 0, 1, 2, 2, 3, 0 };
_vao = gl.GenVertexArray();
gl.BindVertexArray(_vao);
_vbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
unsafe {
fixed (float* p = vertices) {
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
}
}
_ibo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ibo);
unsafe {
fixed (ushort* p = indices) {
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
}
}
// Quad attributes
gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
// Instance attributes
_instanceVbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
// iPosition
gl.EnableVertexAttribArray(2);
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
gl.VertexAttribDivisor(2, 1);
// iScaleOpacityActive
gl.EnableVertexAttribArray(3);
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
gl.VertexAttribDivisor(3, 1);
// iTextureIndex
gl.EnableVertexAttribArray(4);
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
gl.VertexAttribDivisor(4, 1);
// iRotation (Quaternion)
gl.EnableVertexAttribArray(5);
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
gl.VertexAttribDivisor(5, 1);
// iSize
gl.EnableVertexAttribArray(6);
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
gl.VertexAttribDivisor(6, 1);
// iIsBillboard
gl.EnableVertexAttribArray(7);
gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
gl.VertexAttribDivisor(7, 1);
gl.BindVertexArray(0);
_shader.Bind();
_shader.SetUniform("uTextureArray", 0);
_shader.Unbind();
}
public void Begin(Matrix4x4 viewProjection, Vector3 cameraUp, Vector3 cameraRight) {
_viewProjection = viewProjection;
_cameraUp = cameraUp;
_cameraRight = cameraRight;
_allParticles.Clear();
}
public void AddParticle(ManagedGLTextureArray? atlas, bool isAdditive, ParticleInstance instance, float distanceSq) {
_allParticles.Add(new ParticleRenderData {
Instance = instance,
DistanceSq = distanceSq,
Atlas = atlas,
IsAdditive = isAdditive
});
}
public void Flush() {
if (_allParticles.Count == 0) return;
// Sort back-to-front
_allParticles.Sort((a, b) => b.DistanceSq.CompareTo(a.DistanceSq));
var gl = _graphicsDevice.GL;
gl.BindVertexArray(_vao);
gl.DepthMask(false);
gl.Enable(EnableCap.DepthTest);
gl.Disable(EnableCap.StencilTest);
gl.Disable(EnableCap.CullFace);
gl.Disable(EnableCap.SampleAlphaToCoverage);
gl.Disable(EnableCap.SampleAlphaToOne);
gl.Enable(EnableCap.Blend);
int i = 0;
while (i < _allParticles.Count) {
var p = _allParticles[i];
_currentAtlas = p.Atlas;
_currentIsAdditive = p.IsAdditive;
_currentInstanceCount = 0;
while (i < _allParticles.Count && _allParticles[i].Atlas == _currentAtlas && _allParticles[i].IsAdditive == _currentIsAdditive) {
_instanceData[_currentInstanceCount++] = _allParticles[i].Instance;
i++;
if (_currentInstanceCount >= MAX_PARTICLES_TOTAL) break;
}
if (_currentInstanceCount > 0 && _currentAtlas != null) {
if (_currentIsAdditive) {
gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
}
else {
gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
}
gl.ActiveTexture(TextureUnit.Texture0);
gl.BindTexture(GLEnum.Texture2DArray, (uint)_currentAtlas.NativePtr);
RenderStateCache.CurrentAtlas = (uint)_currentAtlas.Slot;
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
unsafe {
fixed (ParticleInstance* pData = _instanceData) {
gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (uint)(_currentInstanceCount * Marshal.SizeOf<ParticleInstance>()), pData);
}
}
_shader.Bind();
_shader.SetUniform("uViewProjection", _viewProjection);
_shader.SetUniform("uCameraUp", _cameraUp);
_shader.SetUniform("uCameraRight", _cameraRight);
gl.DrawElementsInstanced(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedShort, (void*)0, (uint)_currentInstanceCount);
}
}
gl.DepthMask(true);
_allParticles.Clear();
RenderStateCache.CurrentVAO = 0;
RenderStateCache.CurrentIBO = 0;
}
public void End() {
Flush();
}
public void Dispose() {
var gl = _graphicsDevice.GL;
gl.DeleteVertexArray(_vao);
gl.DeleteBuffer(_vbo);
gl.DeleteBuffer(_instanceVbo);
gl.DeleteBuffer(_ibo);
(_shader as IDisposable)?.Dispose();
}
}
}