phase(N.5) Task 9: BuildIndirectArrays — CPU layout for indirect dispatch
Pure CPU helper that lays out a group list into a contiguous indirect buffer (DrawElementsIndirectCommand[]) and parallel BatchData[] — opaque section first, transparent section second. Returns counts + byte offset for the transparent section. Tests cover: spec §5 walk-through layout; empty group list edge case; ClipMap classification (treated as opaque, not transparent). Static + public so tests can exercise without a GL context. Task 10 wires it into the rewritten Draw() method. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
424d7b9015
commit
9a7a250b62
2 changed files with 196 additions and 0 deletions
|
|
@ -529,6 +529,108 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
_gl.DeleteBuffer(_indirectBuffer);
|
_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.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Public view of the per-group inputs to <see cref="BuildIndirectArrays"/> — used in tests.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct IndirectGroupInput(
|
||||||
|
int IndexCount,
|
||||||
|
uint FirstIndex,
|
||||||
|
int BaseVertex,
|
||||||
|
int InstanceCount,
|
||||||
|
int FirstInstance,
|
||||||
|
ulong TextureHandle,
|
||||||
|
uint TextureLayer,
|
||||||
|
TranslucencyKind Translucency);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Public mirror of the per-group <see cref="BatchData"/> uploaded to the SSBO.
|
||||||
|
/// Tests verify the layout. Same field shape as the private BatchData.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||||
|
public struct BatchDataPublic
|
||||||
|
{
|
||||||
|
public ulong TextureHandle;
|
||||||
|
public uint TextureLayer;
|
||||||
|
public uint Flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Result of <see cref="BuildIndirectArrays"/>.</summary>
|
||||||
|
public readonly record struct IndirectLayoutResult(
|
||||||
|
int OpaqueCount,
|
||||||
|
int TransparentCount,
|
||||||
|
int TransparentByteOffset);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Classification: Opaque + ClipMap → opaque pass (ClipMap uses discard, not
|
||||||
|
/// blending). Everything else (AlphaBlend, Additive, InvAlpha) → transparent pass.
|
||||||
|
/// </remarks>
|
||||||
|
public static IndirectLayoutResult BuildIndirectArrays(
|
||||||
|
IReadOnlyList<IndirectGroupInput> 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(
|
private readonly record struct GroupKey(
|
||||||
uint Ibo,
|
uint Ibo,
|
||||||
uint FirstIndex,
|
uint FirstIndex,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure CPU test of <see cref="WbDrawDispatcher.BuildIndirectArrays"/>.
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<WbDrawDispatcher.IndirectGroupInput>
|
||||||
|
{
|
||||||
|
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<WbDrawDispatcher.IndirectGroupInput>();
|
||||||
|
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<WbDrawDispatcher.IndirectGroupInput>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue