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
|
|
@ -15,16 +15,19 @@ namespace AcDream.App.Rendering.Walk;
|
|||
/// <param name="Records">Already-classified <see cref="RenderProjectionRecord"/>s
|
||||
/// for this turn's cell/building, in the SAME order they must enter the walk
|
||||
/// stream (never re-sorted downstream — <see cref="WalkStaticStreamPopulator"/>'s
|
||||
/// own contract).</param>
|
||||
/// own contract). Campaign FW3.4a: a segment INTO <see cref="WalkProductionWorldData"/>'s
|
||||
/// per-frame arena, not a freshly allocated array — see that type's own doc
|
||||
/// comment.</param>
|
||||
/// <param name="TupleLandblockId">The clip-slot-resolving landblock id
|
||||
/// <c>WbDrawDispatcher.ClassifyEntityForWalk</c> needs per record (FW3.2a's
|
||||
/// <c>tupleLandblockId</c> parameter) — carried per-turn rather than once per
|
||||
/// frame because a single frame's cells/buildings can span more than one
|
||||
/// committed landblock.</param>
|
||||
internal readonly record struct WalkFrameStaticRecords(
|
||||
RenderProjectionRecord[] Records, uint TupleLandblockId)
|
||||
ArraySegment<RenderProjectionRecord> Records, uint TupleLandblockId)
|
||||
{
|
||||
public static readonly WalkFrameStaticRecords Empty = new(Array.Empty<RenderProjectionRecord>(), 0);
|
||||
public static readonly WalkFrameStaticRecords Empty =
|
||||
new(ArraySegment<RenderProjectionRecord>.Empty, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -72,12 +75,11 @@ internal interface IWalkFrameWorldData
|
|||
/// <c>TerrainModernRenderer</c>, <c>GameSky</c>, and
|
||||
/// <c>PortalDepthMaskRenderer.DrawDepthFan</c> — FW3.2b-2's job.
|
||||
///
|
||||
/// <para>Stream submission itself (<c>WbDrawDispatcher.SubmitOrderedStream</c>)
|
||||
/// is deliberately NOT part of this interface: it is already a real,
|
||||
/// FW2/FW3.2a-tested production method, so <see cref="WalkFrameDriver"/>
|
||||
/// calls it directly (plan §FW3.2b-1's "the driver calls
|
||||
/// WbDrawDispatcher.SubmitOrderedStream" wording) rather than abstracting a
|
||||
/// method that would just forward to it one layer deeper.</para>
|
||||
/// <para>Stream submission itself (<c>WbDrawDispatcher.PrepareOrderedStream</c>/
|
||||
/// <c>DrawOrderedRange</c>) is deliberately NOT part of this interface: it is
|
||||
/// already real, production-tested machinery, so <see cref="WalkFrameDriver"/>
|
||||
/// calls it directly rather than abstracting a method that would just
|
||||
/// forward to it one layer deeper.</para>
|
||||
/// </summary>
|
||||
internal interface IWalkFrameLeafRenderer
|
||||
{
|
||||
|
|
@ -90,7 +92,7 @@ internal interface IWalkFrameLeafRenderer
|
|||
|
||||
/// <summary><c>LScape::grab_visible_cells</c>'s terrain mesh, once per
|
||||
/// ACTIVE clip slice — <paramref name="sliceIndex"/> is caller-supplied
|
||||
/// (<see cref="WalkFrameDriver.RunFrame"/>'s <c>activeTerrainSliceCount</c>)
|
||||
/// (<see cref="WalkFrameDriver.Collect"/>'s <c>activeTerrainSliceCount</c>)
|
||||
/// since FW3.2b-1 does not wire <c>ClipFrameAssembler</c>/
|
||||
/// <c>ViewconeCuller</c> (FW3.2b-2's job — see plan §FW3.2's dynamic-route
|
||||
/// survival note). Terrain draws FULLY before any per-cell building/
|
||||
|
|
@ -143,19 +145,18 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// punch fan — pass 1 of the building portal walk.
|
||||
/// <paramref name="worldPolygon"/> is already transformed building-local
|
||||
/// → world (<see cref="WalkFrameDriver"/> does the transform via
|
||||
/// <see cref="IWalkFrameWorldData.GetBuildingWorldTransform"/> before
|
||||
/// calling this). The real implementation is
|
||||
/// <see cref="IWalkFrameWorldData.GetBuildingWorldTransform"/> at Collect
|
||||
/// time — see that type's own doc comment). The real implementation is
|
||||
/// <c>PortalDepthMaskRenderer.DrawDepthFan</c> with <c>forceFarZ</c>
|
||||
/// (FW3.2b-2 wiring) — this stage only proves the CALL happens at the
|
||||
/// right point in walk order: the driver flushes the accumulated stream
|
||||
/// segment immediately before this call, so ANY content queued ahead of
|
||||
/// the punch (a preceding cell's/building's contents — never this
|
||||
/// building's OWN shell, which retail draws only after the whole portal
|
||||
/// walk completes; see <see cref="WalkFrameDriver"/>'s type doc comment)
|
||||
/// reaches the GPU first. <paramref name="activeViewIndex"/> is the view
|
||||
/// the emitting two-pass walk was pinned to (retail
|
||||
/// <c>building_view = Render::portal_view_num</c> @0x0059f3bf) —
|
||||
/// production clips the fan by that view's slice planes.</summary>
|
||||
/// (FW3.2b-2 wiring) — Replay calls this at exactly the point Collect
|
||||
/// recorded it: any content queued ahead of the punch (a preceding
|
||||
/// cell's/building's contents — never this building's OWN shell, which
|
||||
/// retail draws only after the whole portal walk completes; see
|
||||
/// <see cref="WalkFrameDriver"/>'s type doc comment) reaches the GPU
|
||||
/// first. <paramref name="activeViewIndex"/> is the view the emitting
|
||||
/// two-pass walk was pinned to (retail <c>building_view =
|
||||
/// Render::portal_view_num</c> @0x0059f3bf) — production clips the fan by
|
||||
/// that view's slice planes.</summary>
|
||||
void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex);
|
||||
|
||||
/// <summary><c>RetailAlphaQueue.FlushFartherThan</c>'s <c>DrawBuilding</c>
|
||||
|
|
@ -173,60 +174,187 @@ internal interface IWalkFrameLeafRenderer
|
|||
|
||||
/// <summary>
|
||||
/// Campaign FW stage FW3.2b-1 test seam: an optional, diagnostic-only
|
||||
/// observer of every stream FLUSH <see cref="WalkFrameDriver"/> performs.
|
||||
/// Production callers pass <see langword="null"/> (the default) — this
|
||||
/// exists purely so the headless referee suite can assert flush COUNT,
|
||||
/// per-flush command count, and per-flush stage without re-deriving them
|
||||
/// observer of every ordered-stream range <see cref="WalkFrameDriver.Replay"/>
|
||||
/// draws. Production callers pass <see langword="null"/> (the default) — this
|
||||
/// exists purely so the headless referee suite can assert range COUNT,
|
||||
/// per-range command count, and per-range stage without re-deriving them
|
||||
/// from <c>RecordingGpuDevice.Calls</c>' lower-level RHI call log.
|
||||
/// </summary>
|
||||
internal interface IWalkFrameDriverTrace
|
||||
{
|
||||
/// <summary><paramref name="stages"/> is a snapshot (never the live,
|
||||
/// about-to-be-<c>Reset</c> list) of every command's
|
||||
/// <see cref="WalkDrawStage"/> in the flushed segment, in stream order —
|
||||
/// by this stage's own flush discipline (flush before every non-stream
|
||||
/// leaf action) a segment is always single-stage in practice, but the
|
||||
/// full list is passed so a test can assert that invariant itself
|
||||
/// instead of trusting it.</summary>
|
||||
/// <summary><paramref name="stages"/> is a snapshot (never a live,
|
||||
/// about-to-mutate list) of every command's <see cref="WalkDrawStage"/> in
|
||||
/// the drawn segment, in stream order — by this stage's own segmenting
|
||||
/// discipline (a segment boundary before every non-stream leaf action) a
|
||||
/// segment is always single-stage in practice, but the full list is
|
||||
/// passed so a test can assert that invariant itself instead of trusting
|
||||
/// it.</summary>
|
||||
void OnFlush(int commandCount, IReadOnlyList<WalkDrawStage> stages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW3.4a: one turn Collect recorded, replayed by
|
||||
/// <see cref="WalkFrameDriver.Replay"/> in the exact order Collect saw it.
|
||||
/// <see cref="WalkFrameEventKind.StreamMark"/> is the collect-time analogue of
|
||||
/// the old immediate driver's flush point — see <see cref="WalkFrameEventKind"/>'s
|
||||
/// own doc comment for the full list and what each carries.
|
||||
/// </summary>
|
||||
internal enum WalkFrameEventKind : byte
|
||||
{
|
||||
/// <summary>The accumulated <see cref="OrderedDrawStream"/> grew since the
|
||||
/// last mark and must be drawn, via <c>WbDrawDispatcher.DrawOrderedRange</c>,
|
||||
/// before whatever leaf event follows. <see cref="WalkFrameEvent.IntArg"/>
|
||||
/// is the stream's exclusive-end command index at the moment this event
|
||||
/// was recorded.</summary>
|
||||
StreamMark,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawSky"/>.</summary>
|
||||
Sky,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawTerrainSlice"/> —
|
||||
/// <see cref="WalkFrameEvent.IntArg"/> is the slice index.</summary>
|
||||
TerrainSlice,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
|
||||
/// <see cref="WalkFrameEvent.CellId"/> is the cell.</summary>
|
||||
CellShell,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> —
|
||||
/// <see cref="WalkFrameEvent.Polygon"/> is the already-world-transformed
|
||||
/// polygon (transformed at Collect time, exactly as the pre-FW3.4a driver
|
||||
/// transformed it before its own immediate call), <see cref="WalkFrameEvent.IntArg"/>
|
||||
/// is the active view index.</summary>
|
||||
PunchFan,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.AlphaBarrier"/> —
|
||||
/// <see cref="WalkFrameEvent.FloatArg"/> is the viewer distance, computed
|
||||
/// at Collect time (the context that supplies it does not outlive Collect).</summary>
|
||||
AlphaBarrier,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.ClearInteriorDepth"/>.</summary>
|
||||
ClearInteriorDepth,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawExitSeals"/>.</summary>
|
||||
ExitSeals,
|
||||
}
|
||||
|
||||
/// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per
|
||||
/// kind. A single struct (rather than a kind hierarchy) keeps Collect's
|
||||
/// per-turn list a flat, allocation-cheap <c>List<WalkFrameEvent></c> —
|
||||
/// only <see cref="Polygon"/> (a punch fan's already-transformed geometry)
|
||||
/// allocates, and only once per punch, which is rare enough per frame to be
|
||||
/// unconditionally acceptable (plan §FW3.4a's own call).</summary>
|
||||
internal readonly struct WalkFrameEvent
|
||||
{
|
||||
private WalkFrameEvent(
|
||||
WalkFrameEventKind kind, int intArg, uint cellId, float floatArg, WalkPolygon? polygon)
|
||||
{
|
||||
Kind = kind;
|
||||
IntArg = intArg;
|
||||
CellId = cellId;
|
||||
FloatArg = floatArg;
|
||||
Polygon = polygon;
|
||||
}
|
||||
|
||||
internal WalkFrameEventKind Kind { get; }
|
||||
|
||||
internal int IntArg { get; }
|
||||
|
||||
internal uint CellId { get; }
|
||||
|
||||
internal float FloatArg { get; }
|
||||
|
||||
internal WalkPolygon? Polygon { get; }
|
||||
|
||||
internal static WalkFrameEvent Mark(int exclusiveEnd) =>
|
||||
new(WalkFrameEventKind.StreamMark, exclusiveEnd, 0, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent Sky() =>
|
||||
new(WalkFrameEventKind.Sky, 0, 0, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent TerrainSlice(int sliceIndex) =>
|
||||
new(WalkFrameEventKind.TerrainSlice, sliceIndex, 0, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent CellShell(uint cellId) =>
|
||||
new(WalkFrameEventKind.CellShell, 0, cellId, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent PunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
||||
new(WalkFrameEventKind.PunchFan, activeViewIndex, 0, 0f, worldPolygon);
|
||||
|
||||
internal static WalkFrameEvent AlphaBarrier(float viewerDistance) =>
|
||||
new(WalkFrameEventKind.AlphaBarrier, 0, 0, viewerDistance, null);
|
||||
|
||||
internal static WalkFrameEvent ClearInteriorDepth() =>
|
||||
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent ExitSeals() =>
|
||||
new(WalkFrameEventKind.ExitSeals, 0, 0, 0f, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW stage FW3.2b-1 — THE WALK FRAME DRIVER. Executes one full
|
||||
/// static-content frame by driving <see cref="RetailFrameWalk"/> with itself
|
||||
/// as the <see cref="IWalkEventSink"/>, turning each walk turn into either a
|
||||
/// stream append (<see cref="WalkStaticStreamPopulator"/>, FW3.2a) or a leaf
|
||||
/// call (<see cref="IWalkFrameLeafRenderer"/>), so that GPU command-buffer
|
||||
/// order equals retail's walk order (plan §FW3.2b-1's "INTERLEAVING RULE").
|
||||
/// NOT rooted into any production caller yet — <c>WorldSceneRenderer</c>
|
||||
/// does not construct or call this class (FW3.2b-2's job); this stage's
|
||||
/// deliverable is the driver plus a headless referee suite on
|
||||
/// <c>RecordingGpuDevice</c> proving the interleaving.
|
||||
/// as the <see cref="IWalkEventSink"/>, so that GPU command-buffer order
|
||||
/// equals retail's walk order (plan §FW3.2b-1's "INTERLEAVING RULE").
|
||||
///
|
||||
/// <para><b>The one flush rule that reproduces the whole frame script:</b>
|
||||
/// before EVERY leaf-renderer call (<see cref="IWalkFrameLeafRenderer.DrawSky"/>,
|
||||
/// <c>DrawTerrainSlice</c>, <c>DrawCellShell</c>, <c>ClearInteriorDepth</c>,
|
||||
/// <c>DrawExitSeals</c>, <c>DrawPunchFan</c>) and before every
|
||||
/// <see cref="IWalkFrameLeafRenderer.AlphaBarrier"/> call, the driver
|
||||
/// flushes the accumulated opaque stream (a no-op when the stream is empty —
|
||||
/// "empty segments submit nothing"); a building's own shell content is
|
||||
/// APPENDED (not flushed) the moment <see cref="IWalkEventSink.OnBuildingShellTurn"/>
|
||||
/// fires, so it flushes only at whatever non-stream action comes next (the
|
||||
/// next building's alpha barrier, or end of frame). This single rule,
|
||||
/// combined with "shell before contents" per cell, retail's own building
|
||||
/// order (alpha barrier → portal pass → shell — see
|
||||
/// <para><b>Campaign FW3.4a — the ONE-walk split.</b> Before this stage, a
|
||||
/// single frame ran <see cref="RetailFrameWalk"/> TWICE — once with a
|
||||
/// set-collecting sink to learn the flood/visited-cell set before
|
||||
/// <c>PrepareCellBatches</c>, once more through this driver to actually
|
||||
/// submit — and each walk turn's stream content flushed IMMEDIATELY through
|
||||
/// its own full <c>WbDrawDispatcher.SubmitOrderedStream</c> call (~40 of
|
||||
/// those per frame at a town, each rewriting and rebinding all nine
|
||||
/// per-instance sections for that turn's handful of instances). The FW3.4
|
||||
/// perf checkpoint measured +33.5% CPU p50 and 14× frame allocation from
|
||||
/// exactly those two costs (plus a third, unrelated one — see
|
||||
/// <see cref="WalkProductionWorldData"/>'s own doc comment) and tripped the
|
||||
/// campaign's ±20% stop rule. This stage collapses both: <see cref="Collect"/>
|
||||
/// runs <see cref="RetailFrameWalk"/> ONCE, doing everything the immediate
|
||||
/// driver used to do EXCEPT the actual GPU submission — stream appends
|
||||
/// accumulate without flushing, every former immediate leaf call records a
|
||||
/// <see cref="WalkFrameEvent"/> instead, and the driver keeps its
|
||||
/// visited-set bookkeeping (absorbing the renderer's old dedicated
|
||||
/// set-collecting sink) so the SAME walk answers both questions.
|
||||
/// <see cref="Replay"/> then performs the actual GPU work afterward:
|
||||
/// <c>WbDrawDispatcher.PrepareOrderedStream</c> uploads the WHOLE frame's
|
||||
/// stream once, and each recorded <see cref="WalkFrameEventKind.StreamMark"/>
|
||||
/// becomes one cheap <c>DrawOrderedRange</c> call over the already-uploaded
|
||||
/// payload — interleaved, in the exact recorded order, with the leaf
|
||||
/// renderer calls the OLD immediate driver made inline. Because Replay walks
|
||||
/// the SAME event sequence Collect recorded at the SAME points the old code
|
||||
/// flushed, GPU command order is unchanged bit-for-bit; only the number of
|
||||
/// walks (two → one) and the shape of the GPU submission (many small
|
||||
/// rebind-and-draw calls → one bind, many cheap draws) changes.
|
||||
/// <see cref="RunFrame"/> remains Collect immediately followed by Replay, for
|
||||
/// callers (today: the headless referee suite) that do not need the split;
|
||||
/// <c>RetailPViewRenderer</c> uses the split directly, since it must run
|
||||
/// <c>PrepareCellBatches</c>/<c>BuildAndBorrow</c> BETWEEN them.</para>
|
||||
///
|
||||
/// <para><b>The one mark rule that reproduces the whole frame script:</b>
|
||||
/// before EVERY leaf-renderer event (<see cref="WalkFrameEventKind.Sky"/>,
|
||||
/// <c>TerrainSlice</c>, <c>CellShell</c>, <c>ClearInteriorDepth</c>,
|
||||
/// <c>ExitSeals</c>, <c>PunchFan</c>) and before every
|
||||
/// <see cref="WalkFrameEventKind.AlphaBarrier"/> event, Collect records a
|
||||
/// <see cref="WalkFrameEventKind.StreamMark"/> if the stream grew since the
|
||||
/// last one (a no-op otherwise — "empty segments submit nothing"); a
|
||||
/// building's own shell content is APPENDED (not marked) the moment
|
||||
/// <see cref="IWalkEventSink.OnBuildingShellTurn"/> fires, so it only gets a
|
||||
/// mark ahead of whatever non-stream event comes next (the next building's
|
||||
/// alpha barrier, or the final mark at <see cref="Replay"/>'s prepare step).
|
||||
/// This single rule, combined with "shell before contents" per cell,
|
||||
/// retail's own building order (alpha barrier → portal pass → shell — see
|
||||
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
|
||||
/// own interior-root DRAW order (landscape → clear → seals → the flood's own
|
||||
/// cells — see <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>'s doc
|
||||
/// comment; this is NOT the order the walk's EVENTS fire in, which is
|
||||
/// breakpoint-entry order matching the FW0 oracle traces), is what produces
|
||||
/// every ordering constraint the plan's frame script names: [cell1 shell]
|
||||
/// [cell1 contents flush] [cell2 shell] …, [alpha barrier] [punch fan(s) +
|
||||
/// [cell1 contents mark] [cell2 shell] …, [alpha barrier] [punch fan(s) +
|
||||
/// look-in flood(s), each following the SAME shell-then-contents per-cell
|
||||
/// discipline] [building shell content flush], [landscape (if exit views
|
||||
/// discipline] [building shell content mark], [landscape (if exit views
|
||||
/// survived)] [interior depth clear] [exit-portal seals] [the interior
|
||||
/// root's own flood cells], and the final end-of-frame flush. No special-
|
||||
/// casing per turn kind is needed beyond that.</para>
|
||||
/// root's own flood cells], and a final mark at Replay's prepare step. No
|
||||
/// special-casing per turn kind is needed beyond that.</para>
|
||||
///
|
||||
/// <para>Retail anchors: <c>SmartBox::RenderNormalMode</c> @0x00453aa0 (the
|
||||
/// root <see cref="RetailFrameWalk.WalkFrame"/> already ports),
|
||||
|
|
@ -248,16 +376,29 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
private readonly IWalkFrameWorldData _worldData;
|
||||
private readonly IWalkFrameDriverTrace? _trace;
|
||||
private readonly OrderedDrawStream _stream = new();
|
||||
private readonly List<WalkFrameEvent> _events = new();
|
||||
private readonly List<int> _markPositions = new();
|
||||
|
||||
// ---- transient per-RunFrame state (set in BeginFrame, cleared in EndFrame) ----
|
||||
// Campaign FW3.4a: visited-set collection, absorbed from the renderer's
|
||||
// former dedicated set-collecting sink (RetailPViewRenderer's old
|
||||
// WalkVisitedSetCollector) — the SAME shapes that sink produced, now
|
||||
// populated by the ONE walk Collect already runs instead of a second
|
||||
// walk pass dedicated to nothing but set-gathering.
|
||||
internal HashSet<uint> VisitedCells { get; } = new();
|
||||
|
||||
internal List<WalkBuilding> VisitedBuildings { get; } = new();
|
||||
|
||||
internal HashSet<uint> VisitedLandscapeCellIds { get; } = new();
|
||||
|
||||
// ---- transient per-Collect state (set in BeginFrame, read by Replay,
|
||||
// cleared by Replay's own completion) ----
|
||||
private IWalkBuildingFrameContext? _ctx;
|
||||
private IGpuFrame? _frame;
|
||||
private IGpuPassEncoder? _encoder;
|
||||
private Matrix4x4 _viewProjection;
|
||||
private Vector3 _cameraWorldPosition;
|
||||
private int _activeTerrainSliceCount;
|
||||
private bool _skyDrawnThisFrame;
|
||||
private WalkDrawStage? _currentDcStage;
|
||||
private bool _readyToReplay;
|
||||
|
||||
internal WalkFrameDriver(
|
||||
WbDrawDispatcher dispatcher,
|
||||
|
|
@ -274,8 +415,10 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
|
||||
/// <summary>
|
||||
/// Drives one complete frame at retail's root (<c>SmartBox::RenderNormalMode</c>):
|
||||
/// calls <see cref="RetailFrameWalk.WalkFrame"/> with this driver as the
|
||||
/// sink, sandwiched between <see cref="BeginFrame"/>/<see cref="EndFrame"/>.
|
||||
/// <see cref="Collect"/> immediately followed by <see cref="Replay"/>. Kept
|
||||
/// for callers that don't need the split (today: the headless referee
|
||||
/// suite) — <c>RetailPViewRenderer</c> calls the pair directly, since it
|
||||
/// must run other frame work BETWEEN them (plan §FW3.4a).
|
||||
/// </summary>
|
||||
internal void RunFrame(
|
||||
RetailFrameWalk walk,
|
||||
|
|
@ -288,35 +431,57 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
Matrix4x4 viewProjection,
|
||||
Vector3 cameraWorldPosition,
|
||||
int activeTerrainSliceCount = 1)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
|
||||
Collect(
|
||||
walk, cameraCellId, cameraCell, landscape, ctx,
|
||||
viewProjection, cameraWorldPosition, activeTerrainSliceCount);
|
||||
Replay(frame, encoder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW3.4a Phase 1 — THE ONE WALK. Drives
|
||||
/// <see cref="RetailFrameWalk.WalkFrame"/> with this driver as its sink,
|
||||
/// sandwiched between <see cref="BeginFrame"/>/<see cref="EndFrame"/>,
|
||||
/// performing NO GPU work: see this type's own doc comment.
|
||||
/// </summary>
|
||||
internal void Collect(
|
||||
RetailFrameWalk walk,
|
||||
uint cameraCellId,
|
||||
WalkCell? cameraCell,
|
||||
WalkLandscape landscape,
|
||||
IRetailFrameWalkContext ctx,
|
||||
Matrix4x4 viewProjection,
|
||||
Vector3 cameraWorldPosition,
|
||||
int activeTerrainSliceCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(walk);
|
||||
ArgumentNullException.ThrowIfNull(landscape);
|
||||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
|
||||
BeginFrame(ctx, frame, encoder, viewProjection, cameraWorldPosition, activeTerrainSliceCount);
|
||||
BeginFrame(ctx, viewProjection, cameraWorldPosition, activeTerrainSliceCount);
|
||||
walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this);
|
||||
EndFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a driver frame without driving the walk itself — for a caller
|
||||
/// Opens a collect scope without driving the walk itself — for a caller
|
||||
/// (or a test) that already holds an isolated walk entry point (e.g. one
|
||||
/// <see cref="RetailFrameWalk.DrawBuilding"/> or
|
||||
/// <see cref="RetailFrameWalk.DrawLandscape"/> call) and wants this
|
||||
/// driver's turn handling without going through the top-level root.
|
||||
/// <see cref="RunFrame"/> is implemented in terms of this pair.
|
||||
/// <see cref="Collect"/> is implemented in terms of this pair. Performs no
|
||||
/// GPU work — see this type's own doc comment.
|
||||
/// </summary>
|
||||
internal void BeginFrame(
|
||||
IWalkBuildingFrameContext ctx,
|
||||
IGpuFrame frame,
|
||||
IGpuPassEncoder encoder,
|
||||
Matrix4x4 viewProjection,
|
||||
Vector3 cameraWorldPosition,
|
||||
int activeTerrainSliceCount)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
if (activeTerrainSliceCount < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
|
|
@ -332,36 +497,112 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
}
|
||||
|
||||
_ctx = ctx;
|
||||
_frame = frame;
|
||||
_encoder = encoder;
|
||||
_viewProjection = viewProjection;
|
||||
_cameraWorldPosition = cameraWorldPosition;
|
||||
_activeTerrainSliceCount = activeTerrainSliceCount;
|
||||
_skyDrawnThisFrame = false;
|
||||
_currentDcStage = null;
|
||||
_readyToReplay = false;
|
||||
_stream.Reset();
|
||||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
VisitedCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
VisitedLandscapeCellIds.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Final segment flush (plan §FW3.2b-1's "at frame end: final
|
||||
/// segment flush"), then clears transient per-frame state. Always runs
|
||||
/// via the caller's <c>try</c>/<c>finally</c> discipline in
|
||||
/// <see cref="RunFrame"/> — a caller driving the walk manually should
|
||||
/// follow the same shape.</summary>
|
||||
/// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame
|
||||
/// end: final segment flush", now a mark rather than a draw — see this
|
||||
/// type's own doc comment), then closes the collect scope. The recorded
|
||||
/// stream/events survive this call — <see cref="Replay"/> consumes them —
|
||||
/// which is the one behavioral difference from the pre-FW3.4a EndFrame,
|
||||
/// which reset the stream here because it had just drawn it.</summary>
|
||||
internal void EndFrame()
|
||||
{
|
||||
try
|
||||
{
|
||||
FlushIfNonEmpty();
|
||||
MarkIfGrown();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ctx = null;
|
||||
_frame = null;
|
||||
_encoder = null;
|
||||
_stream.Reset();
|
||||
_readyToReplay = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW3.4a Phase 2. Requires a completed Collect (an
|
||||
/// <see cref="EndFrame"/> having run since the last Replay) — throws
|
||||
/// otherwise, rather than silently replaying a stale or empty event list.
|
||||
/// Uploads the WHOLE collected stream exactly once (skipped when it is
|
||||
/// empty), then walks the recorded events in order: a
|
||||
/// <see cref="WalkFrameEventKind.StreamMark"/> issues one
|
||||
/// <c>WbDrawDispatcher.DrawOrderedRange</c> call over the segment it
|
||||
/// closes off; every other event kind issues its corresponding
|
||||
/// <see cref="IWalkFrameLeafRenderer"/> call. Because Collect recorded
|
||||
/// these events at EXACTLY the points the pre-FW3.4a immediate driver
|
||||
/// used to flush/draw, this reproduces the SAME interleaved GPU command
|
||||
/// order — the campaign invariant — from one walk instead of two.
|
||||
/// </summary>
|
||||
internal void Replay(IGpuFrame frame, IGpuPassEncoder encoder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(frame);
|
||||
ArgumentNullException.ThrowIfNull(encoder);
|
||||
if (!_readyToReplay)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"WalkFrameDriver.Replay was called without a completed Collect (BeginFrame/"
|
||||
+ "EndFrame, or Collect/RunFrame) preceding it — there is nothing recorded to "
|
||||
+ "replay.");
|
||||
}
|
||||
|
||||
if (_stream.Count > 0)
|
||||
_dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions);
|
||||
|
||||
int cursor = 0;
|
||||
for (int i = 0; i < _events.Count; i++)
|
||||
{
|
||||
WalkFrameEvent e = _events[i];
|
||||
switch (e.Kind)
|
||||
{
|
||||
case WalkFrameEventKind.StreamMark:
|
||||
int end = e.IntArg;
|
||||
int count = end - cursor;
|
||||
if (_trace is not null)
|
||||
_trace.OnFlush(count, _stream.Stages.GetRange(cursor, count));
|
||||
_dispatcher.DrawOrderedRange(encoder, cursor, count);
|
||||
cursor = end;
|
||||
break;
|
||||
case WalkFrameEventKind.Sky:
|
||||
_leafRenderer.DrawSky();
|
||||
break;
|
||||
case WalkFrameEventKind.TerrainSlice:
|
||||
_leafRenderer.DrawTerrainSlice(e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.CellShell:
|
||||
_leafRenderer.DrawCellShell(e.CellId);
|
||||
break;
|
||||
case WalkFrameEventKind.PunchFan:
|
||||
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
|
||||
break;
|
||||
case WalkFrameEventKind.AlphaBarrier:
|
||||
_leafRenderer.AlphaBarrier(e.FloatArg);
|
||||
break;
|
||||
case WalkFrameEventKind.ClearInteriorDepth:
|
||||
_leafRenderer.ClearInteriorDepth();
|
||||
break;
|
||||
case WalkFrameEventKind.ExitSeals:
|
||||
_leafRenderer.DrawExitSeals();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_stream.Reset();
|
||||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
_readyToReplay = false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// IWalkEventSink
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -372,11 +613,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
{
|
||||
case WalkEventKind.DrawInside:
|
||||
_currentDcStage = WalkDrawStage.CellStatic;
|
||||
VisitedCells.Add(walkEvent.CellId);
|
||||
break;
|
||||
case WalkEventKind.Landscape:
|
||||
HandleLandscapeTurn();
|
||||
break;
|
||||
case WalkEventKind.DrawCells:
|
||||
foreach (uint id in walkEvent.Cells)
|
||||
VisitedCells.Add(id);
|
||||
HandleDrawCellsTurn(walkEvent.Cells);
|
||||
break;
|
||||
case WalkEventKind.Building:
|
||||
|
|
@ -389,6 +633,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
|
||||
{
|
||||
RequireOpenFrame();
|
||||
VisitedLandscapeCellIds.Add(cellId);
|
||||
WalkFrameStaticRecords records = _worldData.GetOutdoorStatics(cellId);
|
||||
_populator.PopulateOutdoorStatics(
|
||||
_stream, cellId, records.Records, records.TupleLandblockId,
|
||||
|
|
@ -399,13 +644,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(building);
|
||||
IWalkBuildingFrameContext ctx = RequireOpenFrame();
|
||||
VisitedBuildings.Add(building);
|
||||
|
||||
// D3DPolyRender::FlushAlphaList(0f) @0x0059f30b — retail's alpha
|
||||
// barrier, first inside the gate. The portal pass (punches +
|
||||
// look-ins) follows this call; the building's own shell content is
|
||||
// appended only once that pass completes (OnBuildingShellTurn).
|
||||
FlushIfNonEmpty();
|
||||
_leafRenderer.AlphaBarrier(ctx.ViewerDistanceTo(building));
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.AlphaBarrier(ctx.ViewerDistanceTo(building)));
|
||||
|
||||
_currentDcStage = WalkDrawStage.LookInStatic;
|
||||
}
|
||||
|
|
@ -417,10 +663,10 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
|
||||
// CPhysicsPart::Draw(parts, 0) @0x0059f331 — retail's plain-mesh
|
||||
// shell draw, strictly after the portal pass (CPhysicsPart::Draw
|
||||
// (parts, 1)). Flush first so this building's shell content never
|
||||
// shares a segment with whatever the portal pass's last look-in
|
||||
// flood appended (keeps every flushed segment single-stage).
|
||||
FlushIfNonEmpty();
|
||||
// (parts, 1)). Mark first so this building's shell content never
|
||||
// shares a replayed range with whatever the portal pass's last
|
||||
// look-in flood appended (keeps every range single-stage).
|
||||
MarkIfGrown();
|
||||
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
|
||||
_populator.PopulateCell(
|
||||
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
|
||||
|
|
@ -434,10 +680,10 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
ArgumentNullException.ThrowIfNull(polygon);
|
||||
RequireOpenFrame();
|
||||
|
||||
FlushIfNonEmpty();
|
||||
MarkIfGrown();
|
||||
Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building);
|
||||
_leafRenderer.DrawPunchFan(
|
||||
TransformToWorld(polygon, worldTransform), activeViewIndex);
|
||||
_events.Add(
|
||||
WalkFrameEvent.PunchFan(TransformToWorld(polygon, worldTransform), activeViewIndex));
|
||||
}
|
||||
|
||||
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells)
|
||||
|
|
@ -450,11 +696,11 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
// both unconditional for an interior root's own flood, whether or
|
||||
// not a landscape turn just ran (see this driver's type doc
|
||||
// comment).
|
||||
FlushIfNonEmpty();
|
||||
_leafRenderer.ClearInteriorDepth();
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.ClearInteriorDepth());
|
||||
|
||||
FlushIfNonEmpty();
|
||||
_leafRenderer.DrawExitSeals();
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.ExitSeals());
|
||||
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
EmitCellTurn(WalkDrawStage.CellStatic, cells[i]);
|
||||
|
|
@ -478,11 +724,11 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
+ "FW3.2b-1 fail-loud rule).");
|
||||
}
|
||||
|
||||
FlushIfNonEmpty();
|
||||
_leafRenderer.DrawSky();
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.Sky());
|
||||
_skyDrawnThisFrame = true;
|
||||
for (int slice = 0; slice < _activeTerrainSliceCount; slice++)
|
||||
_leafRenderer.DrawTerrainSlice(slice);
|
||||
_events.Add(WalkFrameEvent.TerrainSlice(slice));
|
||||
}
|
||||
|
||||
private void HandleDrawCellsTurn(IReadOnlyList<uint> cells)
|
||||
|
|
@ -512,38 +758,46 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
// Any other stage (LookInStatic) is a building's look-in flood:
|
||||
// retail calls DrawCells re-entrantly there with no landscape/clear/
|
||||
// seal step, so its DC event already fires at the real draw point —
|
||||
// draw immediately, unchanged from before this correction.
|
||||
// record immediately, unchanged from before this correction.
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
EmitCellTurn(stage, cells[i]);
|
||||
}
|
||||
|
||||
private void EmitCellTurn(WalkDrawStage stage, uint cellId)
|
||||
{
|
||||
FlushIfNonEmpty();
|
||||
_leafRenderer.DrawCellShell(cellId);
|
||||
MarkIfGrown();
|
||||
_events.Add(WalkFrameEvent.CellShell(cellId));
|
||||
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
|
||||
_populator.PopulateCell(
|
||||
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection);
|
||||
}
|
||||
|
||||
private void FlushIfNonEmpty()
|
||||
/// <summary>Campaign FW3.4a: the collect-time analogue of the old
|
||||
/// immediate driver's <c>FlushIfNonEmpty</c> — records a
|
||||
/// <see cref="WalkFrameEventKind.StreamMark"/> at the stream's current
|
||||
/// length if it grew since the last mark (a no-op otherwise, exactly
|
||||
/// like that method's own "empty segments submit nothing" rule). Also
|
||||
/// appends the boundary to <see cref="_markPositions"/>, which
|
||||
/// <see cref="Replay"/> hands to <c>PrepareOrderedStream</c> so a merge
|
||||
/// run can never span it — see <c>WbDrawDispatcher.BuildOrderedMergeRuns</c>'s
|
||||
/// <c>forcedBreaksAscending</c> parameter for why that matters.</summary>
|
||||
private void MarkIfGrown()
|
||||
{
|
||||
if (_stream.Count == 0)
|
||||
int count = _stream.Count;
|
||||
int last = _markPositions.Count > 0 ? _markPositions[^1] : 0;
|
||||
if (count == last)
|
||||
return;
|
||||
|
||||
if (_trace is not null)
|
||||
_trace.OnFlush(_stream.Count, _stream.Stages.ToArray());
|
||||
|
||||
_dispatcher.SubmitOrderedStream(_frame!, _encoder!, _stream, _viewProjection);
|
||||
_stream.Reset();
|
||||
_markPositions.Add(count);
|
||||
_events.Add(WalkFrameEvent.Mark(count));
|
||||
}
|
||||
|
||||
private IWalkBuildingFrameContext RequireOpenFrame() =>
|
||||
_ctx ?? throw new InvalidOperationException(
|
||||
"WalkFrameDriver received a walk turn outside BeginFrame/EndFrame — call "
|
||||
+ "BeginFrame (or RunFrame) before driving the walk with this driver as its "
|
||||
+ "IWalkEventSink.");
|
||||
+ "BeginFrame (or Collect/RunFrame) before driving the walk with this driver as "
|
||||
+ "its IWalkEventSink.");
|
||||
|
||||
/// <summary><c>ConstructBuildingView</c>'s polygon is building-local; the
|
||||
/// punch fan needs world space. Vertices transform directly; the plane
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue