perf(render) Campaign FW3.4a: one walk pass; prepare-once/draw-ranges; arena records
The FW3.4 dense-Arwic pair triggered the +/-20% stop rule (+33.5% CPU p50, 14x frame allocation). This slice removes the three measured costs without changing GPU command order (the referee suites assert identical recorded call sequences): - WalkFrameDriver: Collect (ONE walk per frame - no GPU work; leaf calls and flush points become a recorded event list; the driver absorbed the renderer collection pass and exposes the visited sets) + Replay (prepare the whole stream once, then replay events, interleaving DrawOrderedRange with leaf calls in the exact recorded order). RunFrame = Collect+Replay for existing callers. - WbDrawDispatcher: SubmitOrderedStream split into PrepareOrderedStream (all sections + commands + merge runs uploaded once per frame) and DrawOrderedRange (bind-once latch; per-run pipeline + DrawIdOffset + DrawIndirectRangeRhi). Load-bearing correctness catch from the implementation round: merge runs take FORCED BREAKS at the recorded event marks - whole-stream merging must not fuse two segments that retail separates with a leaf GPU call (shell, punch); the straddle assert stays as a dead-code safety net. - WalkProductionWorldData: WalkFrameStaticRecords carries an ArraySegment into a per-frame grow-only arena; the per-cell fresh-array copies (the 1.9 MB/frame alloc p50) are gone - zero steady-state allocation after warmup. Suites (lead-verified): full Release build 0 warnings; hermetic 6,758/0; Walk lane 209/1; InstalledDat Walk conformance 40/1 untouched. Next: the dense-Arwic re-measure against the same-session baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6301e4dbea
commit
212f5a12e5
7 changed files with 1051 additions and 359 deletions
|
|
@ -19,9 +19,11 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
|
||||
/// <summary>
|
||||
/// Campaign FW stage FW2: <see cref="WbDrawDispatcher.BuildOrderedMergeRuns"/>
|
||||
/// (pure CPU merge-run legality) and <see cref="WbDrawDispatcher.SubmitOrderedStream"/>
|
||||
/// (the same legality proven through actual recorded RHI calls against
|
||||
/// <see cref="RecordingGpuDevice"/>).
|
||||
/// (pure CPU merge-run legality) and, from Campaign FW stage FW3.4a,
|
||||
/// <see cref="WbDrawDispatcher.PrepareOrderedStream"/> +
|
||||
/// <see cref="WbDrawDispatcher.DrawOrderedRange"/> (the same legality proven
|
||||
/// through actual recorded RHI calls against <see cref="RecordingGpuDevice"/>,
|
||||
/// replacing the single <c>SubmitOrderedStream</c> call those two now split).
|
||||
/// </summary>
|
||||
public sealed class OrderPreservingSubmitterTests
|
||||
{
|
||||
|
|
@ -197,10 +199,24 @@ public sealed class OrderPreservingSubmitterTests
|
|||
Assert.Equal(stream.Count, totalCommands);
|
||||
}
|
||||
|
||||
// ── SubmitOrderedStream — recorded RHI calls against RecordingGpuDevice ─
|
||||
// ── PrepareOrderedStream + DrawOrderedRange — recorded RHI calls against
|
||||
// RecordingGpuDevice. Campaign FW3.4a replaced the single
|
||||
// SubmitOrderedStream call with this pair (prepare the whole stream once,
|
||||
// draw it via one or more ranges) — every test below that used to call
|
||||
// SubmitOrderedStream now calls Prepare once and Draw the WHOLE stream as
|
||||
// ONE range, which is exactly SubmitOrderedStream's old behavior; the
|
||||
// "several ranges" and "bind once" shapes get their own tests further
|
||||
// down since they have no FW2 analogue. ────────────────────────────────
|
||||
|
||||
private static void PrepareAndDrawWhole(WbDrawDispatcher dispatcher, DrawScope draw, OrderedDrawStream stream)
|
||||
{
|
||||
dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity);
|
||||
if (stream.Count > 0)
|
||||
dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_AlternatingStateCommandsRecordOneDrawEachInOrder()
|
||||
public void PrepareThenDraw_AlternatingStateCommandsRecordOneDrawEachInOrder()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -211,14 +227,14 @@ public sealed class OrderPreservingSubmitterTests
|
|||
MakeCommand(2, translucency: TranslucencyKind.Opaque),
|
||||
MakeCommand(3, translucency: TranslucencyKind.AlphaBlend));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device);
|
||||
Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)], ranges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_MergesAdjacentSameStateCommandsIntoOneMultiDrawIndirect()
|
||||
public void PrepareThenDraw_MergesAdjacentSameStateCommandsIntoOneMultiDrawIndirect()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -226,14 +242,14 @@ public sealed class OrderPreservingSubmitterTests
|
|||
OrderedDrawStream stream = StreamOf(
|
||||
MakeCommand(0), MakeCommand(1), MakeCommand(2));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device);
|
||||
Assert.Equal([(0, 3)], ranges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_CullModeChangeRecordsSeparateCullCallsAndSplitsTheDraw()
|
||||
public void PrepareThenDraw_CullModeChangeRecordsSeparateCullCallsAndSplitsTheDraw()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -243,7 +259,7 @@ public sealed class OrderPreservingSubmitterTests
|
|||
MakeCommand(1, cullMode: CullMode.None),
|
||||
MakeCommand(2, cullMode: CullMode.Clockwise));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
Assert.Equal([(0, 2), (2, 1)], DecodeDrawRanges(fx.Device));
|
||||
|
||||
|
|
@ -254,7 +270,7 @@ public sealed class OrderPreservingSubmitterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_StageChangeSplitsTheDrawEvenWithIdenticalMaterialState()
|
||||
public void PrepareThenDraw_StageChangeSplitsTheDrawEvenWithIdenticalMaterialState()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -263,13 +279,13 @@ public sealed class OrderPreservingSubmitterTests
|
|||
MakeCommand(0, stage: WalkDrawStage.Terrain),
|
||||
MakeCommand(1, stage: WalkDrawStage.CellStatic));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_ADetailCategoryCommandRecordsItsOwnSoloDraw()
|
||||
public void PrepareThenDraw_ADetailCategoryCommandRecordsItsOwnSoloDraw()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -279,13 +295,13 @@ public sealed class OrderPreservingSubmitterTests
|
|||
MakeCommand(1, detailCategory: 1),
|
||||
MakeCommand(2));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
Assert.Equal([(0, 1), (1, 1), (2, 1)], DecodeDrawRanges(fx.Device));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne()
|
||||
public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -294,7 +310,7 @@ public sealed class OrderPreservingSubmitterTests
|
|||
MakeCommand(0, translucency: TranslucencyKind.Opaque),
|
||||
MakeCommand(1, translucency: TranslucencyKind.AlphaBlend));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
|
||||
Assert.Equal(2, runs.Count);
|
||||
|
|
@ -303,7 +319,7 @@ public sealed class OrderPreservingSubmitterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_ThrowsNotSupportedForAPortalPunchCommandBeforeAnyDraw()
|
||||
public void PrepareOrderedStream_ThrowsNotSupportedForAPortalPunchCommandBeforeAnyDraw()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -312,32 +328,165 @@ public sealed class OrderPreservingSubmitterTests
|
|||
MakeCommand(0, stage: WalkDrawStage.PortalPunch));
|
||||
|
||||
Assert.Throws<NotSupportedException>(
|
||||
() => fx.Dispatcher.SubmitOrderedStream(
|
||||
draw.Frame, draw.Pass, stream, Matrix4x4.Identity));
|
||||
() => fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity));
|
||||
|
||||
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedStorageBind>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_EmptyStreamRecordsNoDraws()
|
||||
public void PrepareOrderedStream_EmptyStreamRecordsNoDraws()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(
|
||||
draw.Frame, draw.Pass, new OrderedDrawStream(), Matrix4x4.Identity);
|
||||
fx.Dispatcher.PrepareOrderedStream(draw.Frame, new OrderedDrawStream(), Matrix4x4.Identity);
|
||||
|
||||
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
}
|
||||
|
||||
// ── Campaign FW3.4a — the shapes with no FW2 analogue: drawing the SAME
|
||||
// prepared stream as several ranges, the bind-once optimization, the
|
||||
// fail-loud range check, and the assert-don't-slice straddle guard. ───
|
||||
|
||||
/// <summary>
|
||||
/// The whole point of the split: drawing the SAME stream as TWO ranges
|
||||
/// (with the boundary between them supplied to Prepare, exactly as
|
||||
/// WalkFrameDriver.Replay supplies its recorded mark positions) produces
|
||||
/// the identical total recorded draw/cull/push-constant calls as drawing
|
||||
/// it as one range — the range split changes nothing about what reaches
|
||||
/// the GPU, only how many DrawOrderedRange calls got there.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawOrderedRange_AsTwoRangesAtASegmentBoundary_MatchesOneRangeOverTheWholeStream()
|
||||
{
|
||||
OrderedDrawStream stream = StreamOf(
|
||||
MakeCommand(0, translucency: TranslucencyKind.Opaque),
|
||||
MakeCommand(1, translucency: TranslucencyKind.Opaque),
|
||||
MakeCommand(2, translucency: TranslucencyKind.Opaque),
|
||||
MakeCommand(3, translucency: TranslucencyKind.Opaque));
|
||||
|
||||
using var wholeFx = new DispatcherFixture();
|
||||
using (DrawScope draw = wholeFx.BeginDraw())
|
||||
{
|
||||
wholeFx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity);
|
||||
wholeFx.Dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count);
|
||||
}
|
||||
List<(int Start, int Count)> wholeRanges = DecodeDrawRanges(wholeFx.Device);
|
||||
|
||||
using var splitFx = new DispatcherFixture();
|
||||
using (DrawScope draw = splitFx.BeginDraw())
|
||||
{
|
||||
// Command 2 is a segment boundary (mirrors a mark WalkFrameDriver
|
||||
// would record there, e.g. a cell shell between two same-state
|
||||
// segments) — without it, all four commands would merge into ONE
|
||||
// run; the boundary forces two.
|
||||
splitFx.Dispatcher.PrepareOrderedStream(
|
||||
draw.Frame, stream, Matrix4x4.Identity, forcedBreaksAscending: [2]);
|
||||
splitFx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2);
|
||||
splitFx.Dispatcher.DrawOrderedRange(draw.Pass, 2, 2);
|
||||
}
|
||||
List<(int Start, int Count)> splitRanges = DecodeDrawRanges(splitFx.Device);
|
||||
|
||||
// The split path draws two runs where the whole-range path drew one
|
||||
// (the forced boundary is the only difference) — but every command
|
||||
// reaches the GPU exactly once, in order, with identical coverage.
|
||||
Assert.Equal([(0, 4)], wholeRanges);
|
||||
Assert.Equal([(0, 2), (2, 2)], splitRanges);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The FW3.4a perf shape itself: the nine per-instance storage binds plus
|
||||
/// the warm-up pipeline bind happen on the FIRST DrawOrderedRange call in
|
||||
/// a frame only — a second call over the same prepared payload issues no
|
||||
/// further StorageBind calls, which is the whole reason this stage exists
|
||||
/// (the old SubmitOrderedStream rebound everything on every call).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawOrderedRange_SecondCallInTheSameFrame_BindsNoFurtherStorageSections()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
OrderedDrawStream stream = StreamOf(
|
||||
MakeCommand(0, stage: WalkDrawStage.Terrain),
|
||||
MakeCommand(1, stage: WalkDrawStage.CellStatic));
|
||||
|
||||
fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity);
|
||||
fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 1);
|
||||
int boundAfterFirst = fx.Device.Calls.OfType<GpuRecordedStorageBind>().Count();
|
||||
Assert.True(boundAfterFirst > 0);
|
||||
|
||||
fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1);
|
||||
int boundAfterSecond = fx.Device.Calls.OfType<GpuRecordedStorageBind>().Count();
|
||||
|
||||
Assert.Equal(boundAfterFirst, boundAfterSecond);
|
||||
// Both commands still drew — the bind-once optimization changed
|
||||
// nothing about draw coverage.
|
||||
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fail-loud range check (mirrors DrawPreparedAlphaBatchRhi's): a range
|
||||
/// outside what PrepareOrderedStream uploaded throws rather than drawing
|
||||
/// garbage or silently clamping — including a draw attempted before ANY
|
||||
/// Prepare call this frame.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawOrderedRange_RangeExceedingThePreparedPayload_Throws()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
OrderedDrawStream stream = StreamOf(MakeCommand(0));
|
||||
fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawOrderedRange_BeforeAnyPrepareCallThisFrame_Throws()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plan's "assert it" rule: a range that does not align with a merge
|
||||
/// run boundary throws rather than silently slicing the run — proven
|
||||
/// directly here (skipping the boundary a real WalkFrameDriver mark would
|
||||
/// supply) since production code always supplies the boundary and would
|
||||
/// never exercise this path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawOrderedRange_RangeStraddlingAMergeRun_Throws()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
// All four commands share stage/bucket/cull — ONE merge run [0, 4) —
|
||||
// and no forced break is supplied, so a [0, 2) range straddles it.
|
||||
OrderedDrawStream stream = StreamOf(
|
||||
MakeCommand(0), MakeCommand(1), MakeCommand(2), MakeCommand(3));
|
||||
fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, 2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fail-loud invariant: whatever the state pattern, the recorded
|
||||
/// MultiDrawIndirect calls' DrawCounts always sum to the stream's Count —
|
||||
/// no command is ever silently skipped, and none is drawn twice.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_TotalRecordedDrawCountAlwaysEqualsTheStreamCount()
|
||||
public void PrepareThenDraw_TotalRecordedDrawCountAlwaysEqualsTheStreamCount()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
|
@ -361,7 +510,7 @@ public sealed class OrderPreservingSubmitterTests
|
|||
detailCategory: i == 5 ? 1u : 0u));
|
||||
}
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||
|
||||
List<(int Start, int Count)> ranges = DecodeDrawRanges(fx.Device);
|
||||
int sum = ranges.Sum(r => r.Count);
|
||||
|
|
|
|||
|
|
@ -216,9 +216,9 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.CellStaticsByCell[0x100] = new WalkFrameStaticRecords(
|
||||
[MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)])], 0x8C04u);
|
||||
new[] { MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
worldData.CellStaticsByCell[0x101] = new WalkFrameStaticRecords(
|
||||
[MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)])], 0x8C04u);
|
||||
new[] { MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
|
||||
var leaf = new RecordingLeafRenderer(log);
|
||||
var trace = new RecordingTrace(log);
|
||||
|
|
@ -294,9 +294,9 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.CellStaticsByCell[0x100] = new WalkFrameStaticRecords(
|
||||
[MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)])], 0x8C04u);
|
||||
new[] { MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
worldData.CellStaticsByCell[0x101] = new WalkFrameStaticRecords(
|
||||
[MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)])], 0x8C04u);
|
||||
new[] { MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
|
||||
var leaf = new RecordingLeafRenderer(log);
|
||||
var trace = new RecordingTrace(log);
|
||||
|
|
@ -383,9 +383,9 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.ShellByBuilding[building] = new WalkFrameStaticRecords(
|
||||
[MakeRecord(201, 0, Vector3.Zero, [new MeshRef((uint)shellGfxObj, Matrix4x4.Identity)])], 0x8C04u);
|
||||
new[] { MakeRecord(201, 0, Vector3.Zero, [new MeshRef((uint)shellGfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
worldData.CellStaticsByCell[0x104] = new WalkFrameStaticRecords(
|
||||
[MakeRecord(202, 0, Vector3.Zero, [new MeshRef((uint)interiorGfxObj, Matrix4x4.Identity)])], 0x8C04u);
|
||||
new[] { MakeRecord(202, 0, Vector3.Zero, [new MeshRef((uint)interiorGfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
Matrix4x4 buildingWorld = Matrix4x4.CreateTranslation(10f, 0f, 0f);
|
||||
worldData.WorldTransformByBuilding[building] = buildingWorld;
|
||||
|
||||
|
|
@ -401,9 +401,10 @@ public sealed class WalkFrameDriverTests
|
|||
Assert.Equal(1, activeView.ViewCount);
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
walk.DrawBuilding(building, activeView, ctx, driver);
|
||||
driver.EndFrame();
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104", "FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell" },
|
||||
|
|
@ -433,7 +434,7 @@ public sealed class WalkFrameDriverTests
|
|||
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => ((IWalkEventSink)driver).Emit(WalkEvent.DrawCells(0, [0x100u])));
|
||||
|
|
@ -450,18 +451,82 @@ public sealed class WalkFrameDriverTests
|
|||
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => driver.BeginFrame(
|
||||
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0));
|
||||
ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0));
|
||||
|
||||
driver.EndFrame();
|
||||
// EndFrame cleared the open-frame guard: BeginFrame is usable again.
|
||||
driver.BeginFrame(ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
driver.EndFrame();
|
||||
}
|
||||
|
||||
// ── Campaign FW3.4a fail-loud: Replay without a completed Collect (no
|
||||
// BeginFrame/EndFrame at all, or BeginFrame with no matching EndFrame)
|
||||
// has nothing recorded to draw — throw rather than silently drawing
|
||||
// nothing, which would look like an empty frame instead of a misuse. ──
|
||||
|
||||
[Fact]
|
||||
public void Replay_WithNoPrecedingCollect_Throws()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
Assert.Throws<InvalidOperationException>(() => driver.Replay(draw.Frame, draw.Pass));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replay_WhileCollectIsStillOpen_Throws()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
var ctx = new TestContext();
|
||||
var driver = new WalkFrameDriver(fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData());
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => driver.Replay(draw.Frame, draw.Pass));
|
||||
}
|
||||
|
||||
// ── Deliverable: Collect + Replay called as the SPLIT PAIR (never
|
||||
// RunFrame) — the shape RetailPViewRenderer uses, since it must run other
|
||||
// frame work (PrepareCellBatches/BuildAndBorrow) between the two. Proves
|
||||
// the pair alone — with no GPU work happening until Replay — reproduces
|
||||
// the same turn order RunFrame's combined call would. ──────────────────
|
||||
|
||||
[Fact]
|
||||
public void CollectThenReplay_AsSeparateCalls_PerformsNoGpuWorkUntilReplay()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
var ctx = new TestContext();
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher, new RecordingLeafRenderer(log), new FakeWorldData(), new RecordingTrace(log));
|
||||
var walk = new RetailFrameWalk();
|
||||
// Outdoor root (camera cell low word < 0x100): a minimal, no-op
|
||||
// landscape — LScape::draw still runs its sky/terrain turn against
|
||||
// it even though nothing is published to walk cells/buildings for.
|
||||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.Collect(
|
||||
walk, cameraCellId: 0u, cameraCell: null, landscape, ctx,
|
||||
Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero, activeTerrainSliceCount: 1);
|
||||
|
||||
// No GPU calls at all yet — Collect is CPU-only.
|
||||
Assert.Empty(log);
|
||||
Assert.Empty(fx.Device.Calls);
|
||||
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal(new[] { "SKY", "TERRAIN:0" }, log);
|
||||
}
|
||||
|
||||
// ── Deliverable: an outdoor landscape-cell turn with no building appends
|
||||
// straight to the stream (no shell call — outdoor cells have no EnvCell
|
||||
// shell), and the accumulated content flushes at frame end. ───────────
|
||||
|
|
@ -478,16 +543,18 @@ public sealed class WalkFrameDriverTests
|
|||
var ctx = new TestContext();
|
||||
var worldData = new FakeWorldData();
|
||||
worldData.OutdoorStaticsByCell[0x8C040005u] = new WalkFrameStaticRecords(
|
||||
[MakeRecord(301, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)])], 0x8C04u);
|
||||
new[] { MakeRecord(301, 0, Vector3.Zero, [new MeshRef((uint)gfxObj, Matrix4x4.Identity)]) }, 0x8C04u);
|
||||
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher, new RecordingLeafRenderer(log), worldData, new RecordingTrace(log));
|
||||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero, activeTerrainSliceCount: 0);
|
||||
((IWalkEventSink)driver).OnLandscapeCellTurn(0x8C040005u);
|
||||
Assert.Empty(log); // accumulates in the stream; nothing flushed yet
|
||||
Assert.Empty(log); // no GPU work at Collect time; nothing recorded to the log yet
|
||||
driver.EndFrame();
|
||||
Assert.Empty(log); // still nothing — EndFrame closes Collect, it does not Replay
|
||||
driver.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal(new[] { "FLUSH:1:OutdoorStatic" }, log);
|
||||
GpuRecordedMultiDrawIndirect mdi = Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
/// equivalence referee. Covers <see cref="WbDrawDispatcher.ClassifyEntityForWalk"/>
|
||||
/// (the shared per-entity classify seam), <see cref="WalkStaticStreamPopulator"/>
|
||||
/// (opaque → <see cref="OrderedDrawStream"/>, translucent → the alpha queue,
|
||||
/// selection publish), and the FW3.2a own-cull-scratch fix to
|
||||
/// <c>SubmitOrderedStream</c>.
|
||||
/// selection publish), and the FW3.2a own-cull-scratch fix to the ordered
|
||||
/// submitter (<c>PrepareOrderedStream</c>/<c>DrawOrderedRange</c> as of
|
||||
/// Campaign FW stage FW3.4a).
|
||||
/// </summary>
|
||||
public sealed class WalkStaticStreamPopulatorTests
|
||||
{
|
||||
|
|
@ -364,18 +365,18 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
fx.AlphaQueue.AbortFrame();
|
||||
}
|
||||
|
||||
// ── Deliverable 3: SubmitOrderedStream's own cull scratch ──────────────
|
||||
// ── Deliverable 3: the ordered submitter's own cull scratch ────────────
|
||||
|
||||
[Fact]
|
||||
public void SubmitOrderedStream_DoesNotReadOrCorruptTheSharedAlphaCullScratch()
|
||||
public void PrepareThenDrawOrderedStream_DoesNotReadOrCorruptTheSharedAlphaCullScratch()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
// Poison the SHARED _drawCullModes scratch the alpha path owns — the
|
||||
// exact array SubmitOrderedStream used to write into before FW3.2a.
|
||||
// exact array the ordered submitter used to write into before FW3.2a.
|
||||
// Under the OLD shared-scratch behavior this test's second assertion
|
||||
// fails: SubmitOrderedStream's own command overwrites index 0 with
|
||||
// fails: the ordered path's own command overwrites index 0 with
|
||||
// its own cull mode (Clockwise), destroying the alpha path's poison.
|
||||
FieldInfo field = typeof(WbDrawDispatcher).GetField(
|
||||
"_drawCullModes", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
|
|
@ -388,7 +389,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
|
||||
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));
|
||||
|
||||
fx.Dispatcher.SubmitOrderedStream(draw.Frame, draw.Pass, stream, Matrix4x4.Identity);
|
||||
fx.Dispatcher.PrepareOrderedStream(draw.Frame, stream, Matrix4x4.Identity);
|
||||
fx.Dispatcher.DrawOrderedRange(draw.Pass, 0, stream.Count);
|
||||
|
||||
// (1) The ordered submission's OWN recorded cull call reflects the
|
||||
// STREAM's cull mode (Clockwise -> GpuCullMode.Front), not the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue