phase(N.5) Task 7: dispatcher SSBO + indirect buffer infrastructure

Adds DrawElementsIndirectCommand struct (20-byte layout for
glMultiDrawElementsIndirect). Replaces _instanceVbo field on
WbDrawDispatcher with three buffers: _instanceSsbo (mat4[]),
_batchSsbo (BatchData[]), _indirectBuffer (DEIC[]). Adds BindlessSupport
constructor parameter — non-null required since the dispatcher is only
constructed when WB foundation is on (which implies bindless is present
per Task 6 capability detection).

Existing Draw() method substitutes _instanceVbo -> _instanceSsbo for
compile. Behavior is temporarily wrong (SSBO bound as ArrayBuffer for
per-vertex attribs); Tasks 9-10 fully rewrite the draw loop and the
per-frame uploads to use BindBufferBase + glMultiDrawElementsIndirect.

GameWindow construction site updated to add _bindlessSupport guard and
pass it as the new last argument to the constructor. Dispatcher is only
constructed when bindless is guaranteed present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-08 20:25:29 +02:00
parent 12170f9d78
commit 86c471d2d1
3 changed files with 63 additions and 8 deletions

View file

@ -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)

View file

@ -0,0 +1,17 @@
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Layout matches what <c>glMultiDrawElementsIndirect</c> expects.
/// Total size 20 bytes; arrays are typically uploaded with stride = sizeof(this).
/// </summary>
[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)
}

View file

@ -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<uint> _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(