diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 6c06a97..d6321c9 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -1524,10 +1524,11 @@ public sealed class GameWindow : IDisposable
_staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache, _wbMeshAdapter);
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled
- && _wbMeshAdapter is not null && _wbEntitySpawnAdapter is not null)
+ && _wbMeshAdapter is not null && _wbEntitySpawnAdapter is not null
+ && _bindlessSupport is not null)
{
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
- _gl, _meshShader, _textureCache, _wbMeshAdapter, _wbEntitySpawnAdapter);
+ _gl, _meshShader, _textureCache, _wbMeshAdapter, _wbEntitySpawnAdapter, _bindlessSupport);
}
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
diff --git a/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs
new file mode 100644
index 0000000..80d1119
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/DrawElementsIndirectCommand.cs
@@ -0,0 +1,17 @@
+using System.Runtime.InteropServices;
+
+namespace AcDream.App.Rendering.Wb;
+
+///
+/// Layout matches what glMultiDrawElementsIndirect expects.
+/// Total size 20 bytes; arrays are typically uploaded with stride = sizeof(this).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+public struct DrawElementsIndirectCommand
+{
+ public uint Count; // index count for this draw
+ public uint InstanceCount; // number of instances
+ public uint FirstIndex; // offset into IBO, in indices
+ public int BaseVertex; // vertex offset into VBO
+ public uint BaseInstance; // first instance ID (offsets per-instance attribs / SSBO read)
+}
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
index 4644f71..07d33d1 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Numerics;
+using System.Runtime.InteropServices;
using AcDream.Core.Meshing;
using AcDream.Core.Terrain;
using AcDream.Core.World;
@@ -61,7 +62,32 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private readonly WbMeshAdapter _meshAdapter;
private readonly EntitySpawnAdapter _entitySpawnAdapter;
- private readonly uint _instanceVbo;
+ private readonly BindlessSupport _bindless;
+
+ // SSBO buffer ids
+ private uint _instanceSsbo;
+ private uint _batchSsbo;
+ private uint _indirectBuffer;
+
+ // Per-frame scratch arrays — Tasks 9-10 fully wire these.
+ private float[] _instanceData = new float[256 * 16]; // mat4 floats per instance
+ private BatchData[] _batchData = new BatchData[256];
+ private DrawElementsIndirectCommand[] _indirectCommands = new DrawElementsIndirectCommand[256];
+
+#pragma warning disable CS0169 // Tasks 9-10 wire these counters
+ private int _opaqueDrawCount;
+ private int _transparentDrawCount;
+ private int _transparentByteOffset;
+#pragma warning restore CS0169
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct BatchData
+ {
+ public ulong TextureHandle; // bindless handle (uvec2 in GLSL)
+ public uint TextureLayer;
+ public uint Flags;
+ }
+
private readonly HashSet _patchedVaos = new();
// Per-frame scratch — reused across frames to avoid per-frame allocation.
@@ -89,7 +115,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
Shader shader,
TextureCache textures,
WbMeshAdapter meshAdapter,
- EntitySpawnAdapter entitySpawnAdapter)
+ EntitySpawnAdapter entitySpawnAdapter,
+ BindlessSupport bindless)
{
ArgumentNullException.ThrowIfNull(gl);
ArgumentNullException.ThrowIfNull(shader);
@@ -103,7 +130,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
_meshAdapter = meshAdapter;
_entitySpawnAdapter = entitySpawnAdapter;
- _instanceVbo = _gl.GenBuffer();
+ _bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
+ _instanceSsbo = _gl.GenBuffer();
+ _batchSsbo = _gl.GenBuffer();
+ _indirectBuffer = _gl.GenBuffer();
}
public static Matrix4x4 ComposePartWorldMatrix(
@@ -291,7 +321,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
_opaqueDraws.Sort(static (a, b) => a.SortDistance.CompareTo(b.SortDistance));
// ── Phase 3: one upload of all matrices ─────────────────────────────
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
+ // NOTE: _instanceSsbo is temporarily bound as ArrayBuffer for compile
+ // compatibility. Tasks 9-10 rewrite this to BindBufferBase(SSBO) +
+ // glMultiDrawElementsIndirect.
+ _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceSsbo);
fixed (float* p = _instanceBuffer)
_gl.BufferData(BufferTargetARB.ArrayBuffer,
(nuint)(totalInstances * 16 * sizeof(float)), p, BufferUsageARB.DynamicDraw);
@@ -472,7 +505,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
if (!_patchedVaos.Add(vao)) return;
_gl.BindVertexArray(vao);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
+ // NOTE: temporarily binding _instanceSsbo as ArrayBuffer for compile
+ // compatibility. Tasks 9-10 replace with BindBufferBase(SSBO).
+ _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceSsbo);
for (uint row = 0; row < 4; row++)
{
uint loc = 3 + row;
@@ -494,7 +529,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
{
if (_disposed) return;
_disposed = true;
- _gl.DeleteBuffer(_instanceVbo);
+ _gl.DeleteBuffer(_instanceSsbo);
+ _gl.DeleteBuffer(_batchSsbo);
+ _gl.DeleteBuffer(_indirectBuffer);
}
private readonly record struct GroupKey(