diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 9cf6dc5..2912472 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -529,6 +529,108 @@ public sealed unsafe class WbDrawDispatcher : IDisposable _gl.DeleteBuffer(_indirectBuffer); } + // ── Public types + helpers for BuildIndirectArrays (Task 9) ───────────── + // + // These are public so the pure-CPU unit tests in AcDream.Core.Tests can + // exercise BuildIndirectArrays without needing a GL context. + + /// + /// Public view of the per-group inputs to — used in tests. + /// + public readonly record struct IndirectGroupInput( + int IndexCount, + uint FirstIndex, + int BaseVertex, + int InstanceCount, + int FirstInstance, + ulong TextureHandle, + uint TextureLayer, + TranslucencyKind Translucency); + + /// + /// Public mirror of the per-group uploaded to the SSBO. + /// Tests verify the layout. Same field shape as the private BatchData. + /// + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public struct BatchDataPublic + { + public ulong TextureHandle; + public uint TextureLayer; + public uint Flags; + } + + /// Result of . + public readonly record struct IndirectLayoutResult( + int OpaqueCount, + int TransparentCount, + int TransparentByteOffset); + + /// + /// Lays out the indirect commands + parallel BatchData array contiguously: + /// opaque section first (caller sorts before calling), transparent section second. + /// Pure CPU, no GL state. Caller passes pre-sized scratch arrays. + /// + /// + /// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not + /// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass. + /// + public static IndirectLayoutResult BuildIndirectArrays( + IReadOnlyList groups, + DrawElementsIndirectCommand[] indirectScratch, + BatchDataPublic[] batchScratch) + { + int opaqueCount = 0; + int transparentCount = 0; + + foreach (var g in groups) + { + if (IsOpaque(g.Translucency)) opaqueCount++; + else transparentCount++; + } + + int oi = 0; // opaque write cursor (fills [0..opaqueCount)) + int ti = opaqueCount; // transparent write cursor (fills [opaqueCount..end)) + + foreach (var g in groups) + { + var dec = new DrawElementsIndirectCommand + { + Count = (uint)g.IndexCount, + InstanceCount = (uint)g.InstanceCount, + FirstIndex = g.FirstIndex, + BaseVertex = g.BaseVertex, + BaseInstance = (uint)g.FirstInstance, + }; + var bd = new BatchDataPublic + { + TextureHandle = g.TextureHandle, + TextureLayer = g.TextureLayer, + Flags = 0, + }; + + if (IsOpaque(g.Translucency)) + { + indirectScratch[oi] = dec; + batchScratch[oi] = bd; + oi++; + } + else + { + indirectScratch[ti] = dec; + batchScratch[ti] = bd; + ti++; + } + } + + const int SizeofDEIC = 20; // sizeof(DrawElementsIndirectCommand) — 5 × uint + return new IndirectLayoutResult(opaqueCount, transparentCount, opaqueCount * SizeofDEIC); + } + + private static bool IsOpaque(TranslucencyKind t) + => t == TranslucencyKind.Opaque || t == TranslucencyKind.ClipMap; + + // ──────────────────────────────────────────────────────────────────────── + private readonly record struct GroupKey( uint Ibo, uint FirstIndex, diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherIndirectBuilderTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherIndirectBuilderTests.cs new file mode 100644 index 0000000..1f2e552 --- /dev/null +++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherIndirectBuilderTests.cs @@ -0,0 +1,94 @@ +using System.Numerics; +using AcDream.App.Rendering.Wb; +using AcDream.Core.Meshing; +using Xunit; + +namespace AcDream.Core.Tests.Rendering.Wb; + +/// +/// Pure CPU test of . +/// Verifies that a synthetic group set lays out into the indirect buffer +/// + parallel batch data with opaque section first, transparent second, +/// per-group fields propagated correctly. +/// +public sealed class WbDrawDispatcherIndirectBuilderTests +{ + [Fact] + public void TwoOpaqueGroupsAndOneTransparent_LaysOutContiguouslyOpaqueFirst() + { + // Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances) + var groups = new List + { + new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureHandle: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque), + new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureHandle: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend), + new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureHandle: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque), + }; + + var indirect = new DrawElementsIndirectCommand[16]; + var batch = new WbDrawDispatcher.BatchDataPublic[16]; + + // Act + var result = WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batch); + + // Assert layout + Assert.Equal(2, result.OpaqueCount); + Assert.Equal(1, result.TransparentCount); + Assert.Equal(2 * 20, result.TransparentByteOffset); // sizeof(DEIC) = 20 + + // Opaque section, in input order (Task 10 callers sort) + Assert.Equal(100u, indirect[0].Count); + Assert.Equal(0u, indirect[0].FirstIndex); + Assert.Equal(0, indirect[0].BaseVertex); + Assert.Equal(12u, indirect[0].InstanceCount); + Assert.Equal(0u, indirect[0].BaseInstance); + + Assert.Equal(50u, indirect[1].Count); + Assert.Equal(300u, indirect[1].FirstIndex); + Assert.Equal(100, indirect[1].BaseVertex); + Assert.Equal(1u, indirect[1].InstanceCount); + Assert.Equal(24u, indirect[1].BaseInstance); + + // Transparent section + Assert.Equal(200u, indirect[2].Count); + Assert.Equal(100u, indirect[2].FirstIndex); + Assert.Equal(12u, indirect[2].InstanceCount); + Assert.Equal(12u, indirect[2].BaseInstance); + + // BatchData parallel — same indices as indirect + Assert.Equal(0xAAul, batch[0].TextureHandle); + Assert.Equal(0xCCul, batch[1].TextureHandle); + Assert.Equal(0xBBul, batch[2].TextureHandle); + } + + [Fact] + public void EmptyGroupList_ProducesZeroCounts() + { + var groups = new List(); + var indirect = new DrawElementsIndirectCommand[0]; + var batch = new WbDrawDispatcher.BatchDataPublic[0]; + + var result = WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batch); + + Assert.Equal(0, result.OpaqueCount); + Assert.Equal(0, result.TransparentCount); + Assert.Equal(0, result.TransparentByteOffset); + } + + [Fact] + public void ClipMapTreatedAsOpaque() + { + // ClipMap surfaces (alpha-cutout) belong with the opaque pass + // because the discard handles transparency, not blending. + var groups = new List + { + new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureHandle: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap), + }; + var indirect = new DrawElementsIndirectCommand[4]; + var batch = new WbDrawDispatcher.BatchDataPublic[4]; + + var result = WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batch); + + Assert.Equal(1, result.OpaqueCount); + Assert.Equal(0, result.TransparentCount); + } +}