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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue