fix(render): S3 chunk 3 round 2 — opaque stream marks no longer split terrain batches

Measured (§9.7): the connected R6 soak at e10765aa showed the round-1
batching fix (§9.6 F2) was inert in practice — CPU p50 flat at 31.2 ms
and FPS flat at 32 across every outdoor destination (baseline 2.0-15.5 ms
/ 51-452 FPS), GPU p50 2-4x baseline. Root cause: nearly every admitted
land cell carries scenery statics, so a StreamMark (recorded whenever the
walk's ordered stream grows) follows almost every LandCell event and was
itself treated as a flush point, splitting the pending terrain batch back
down to ~500 submissions/frame — each paying TerrainModernRenderer's full
bind sequence plus a ring allocation and an MDI.

F10: a StreamMark no longer flushes the pending terrain batch. Citation —
the walk's ordered stream holds ONLY opaque batches
(WalkStaticStreamPopulator.cs:179, `if (batch.IsOpaque)` routes translucent
batches to the alpha list instead of the stream) drawn through the
Opaque/OpaqueAlphaToCoverage pipelines, both created with depth test AND
write ON and GpuBlendMode.None (WbDrawDispatcher.Rhi.cs:240-248,343:
`Depth = new GpuDepthState(Test: true, Write: depthWrite, depthCompare)`
with depthWrite: true); DrawOrderedRange resolves every walk-stream
command's bucket to PipelineBucket.Opaque because every command it holds
is opaque (WbDrawDispatcher.OrderedStream.cs:569-571). Opaque terrain and
opaque statics are therefore depth-resolved identically in either
submission order, so deferring the terrain batch across a StreamMark is
pixel-identical (except exact z-ties, which retail itself leaves
order-independent).

Flush points that REMAIN, because their GPU order against terrain IS
observable: PunchFan (DEPTHTEST_ALWAYS + write — the far-Z stamp this
chunk's interleave exists to order correctly), AlphaBarrier and
LandscapeFlush (translucent drains), ClearInteriorDepth, ExitSeals, Sky,
CellShell, a particle turn whose cell has a renderable emitter, and the
end of Replay. AlphaSubmitMark stays a non-flush point (unchanged — it
only enqueues into the CPU alpha list). No ordered-stream stage found
that blends or disables depth write, so no amendment to the contract was
needed; the Replay doc comment records the citation so a future stage
that does must become a flush point again.

F11: no per-batch bind latch added (tens of batches/frame is acceptable).

F12: the F4(c) driver pin is re-expressed to prove a StreamMark from a
cell with real content (not just an empty particle turn) no longer
splits, while PunchFan/AlphaBarrier/a has-emitter particle turn still do;
a new punch-order pin (LandCell(far), building turn with a PunchFan,
LandCell(near)) proves the far terrain flushes before the punch and the
near terrain starts a fresh batch after it. Hermetic App lane: 6,796
passed (6,795 base + 1 new fact), 0 failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 10:28:59 +02:00
parent 0b770afc11
commit 896c63fe9a
2 changed files with 206 additions and 56 deletions

View file

@ -102,15 +102,18 @@ internal interface IWalkFrameLeafRenderer
/// calls this exactly once per frame's Landscape turn).</summary>
void DrawSky();
/// <summary>S3 chunk 3 (§9.2 B1/B2), fix round 1 (F2): retail
/// <c>RenderDeviceD3D::DrawLandCell</c> @0x0059f120 — one or more
/// <summary>S3 chunk 3 (§9.2 B1/B2), fix round 1 (F2), round 2 (F10):
/// retail <c>RenderDeviceD3D::DrawLandCell</c> @0x0059f120 — one or more
/// admitted LOD cells, possibly spanning several landblocks, submitted
/// together as ONE indirect draw. <see cref="WalkFrameDriver.Replay"/>
/// keeps a PENDING batch that <see cref="WalkFrameEventKind.LandCell"/>
/// events append to and flushes (calls this once) immediately before any
/// OTHER event that will itself submit GPU work — see that method's own
/// doc comment for the exact flush points, including the F2 particle-turn
/// exception (a genuinely empty <see cref="DrawStaticParticles"/>/<see
/// event whose GPU order against terrain is OBSERVABLE — see that
/// method's own doc comment for the exact flush-point list and its
/// pipeline-state citation. A <see cref="WalkFrameEventKind.StreamMark"/>
/// is NOT one of them (F10: the ordered stream it draws is opaque-only,
/// depth test AND write on, no blending); the F2 particle-turn exception
/// still holds (a genuinely empty <see cref="DrawStaticParticles"/>/<see
/// cref="DrawCellParticles"/> turn neither submits nor flushes). This is
/// order-preserving by construction: the batch's GPU submission point is
/// always the SAME point the unbatched terrain draws would have occupied
@ -287,11 +290,21 @@ internal enum WalkFrameEventKind : byte
/// @0x0059f120): <see cref="WalkFrameEvent.CellId"/> is the landblock,
/// <see cref="WalkFrameEvent.IntArg"/> packs <c>side &lt;&lt; 8 | cellIndex</c>.
/// <see cref="Replay"/> does NOT draw it immediately: it appends the
/// cell to ONE pending terrain batch that may span landblocks and flushes
/// that batch only before a leaf that will submit GPU work (S3 chunk 3
/// fix round 1, §9.6 F2) — a particle turn whose cell has no renderable
/// emitter submits nothing and keeps the batch open. The GPU submission
/// order is therefore identical to replaying each cell alone.</summary>
/// cell to ONE pending terrain batch that may span landblocks and
/// flushes only at a flush point whose GPU order against terrain is
/// OBSERVABLE — <see cref="PunchFan"/>, <see cref="AlphaBarrier"/>,
/// <see cref="LandscapeFlush"/>, <see cref="ClearInteriorDepth"/>,
/// <see cref="ExitSeals"/>, <see cref="Sky"/>, <see cref="CellShell"/>,
/// a particle turn whose cell has a renderable emitter, or the end of
/// Replay (S3 chunk 3 fix round 2, §9.7 F10; see <see cref="Replay"/>'s
/// own doc comment for the full citation). A <see cref="StreamMark"/> is
/// deliberately NOT a flush point: the ordered stream it draws holds
/// ONLY opaque batches, depth test AND write on, no blending, so opaque
/// terrain and opaque statics resolve identically in either submission
/// order. A particle turn whose cell has no renderable emitter submits
/// nothing and keeps the batch open (fix round 1, F2). The GPU
/// submission order is therefore identical to replaying each cell
/// alone.</summary>
LandCell,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
@ -580,12 +593,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
private readonly List<int> _floodViewRouteScratch = new();
private WalkPlane _lookInCyPlane;
/// <summary>S3 chunk 3 (§9.2 B2), fix round 1 (F2): <see cref="Replay"/>'s
/// PENDING terrain batch — every <see cref="WalkFrameEventKind.LandCell"/>
/// event since the last flush, across however many landblocks, in event
/// order. Cleared at the start of each <see cref="Replay"/> call ("frame
/// start") and by every flush; NOT cleared between events, since it is
/// the whole point of the deferred-batching rule.</summary>
/// <summary>S3 chunk 3 (§9.2 B2), fix round 1 (F2), round 2 (F10):
/// <see cref="Replay"/>'s PENDING terrain batch — every
/// <see cref="WalkFrameEventKind.LandCell"/> event since the last flush,
/// across however many landblocks and however many <see cref="WalkFrameEventKind.StreamMark"/>
/// events (F10: not a flush point), in event order. Cleared at the start
/// of each <see cref="Replay"/> call ("frame start") and by every flush;
/// NOT cleared between events, since it is the whole point of the
/// deferred-batching rule.</summary>
private readonly List<(uint LandblockId, int SideCellCount, int CellIndex)> _pendingTerrainBatch = new();
private readonly HashSet<uint> _cellShellsDrawnThisFrame = new();
@ -932,6 +947,49 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
/// 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.
///
/// <para><b>S3 chunk 3 fix round 2 (§9.7 F10) — the pending terrain
/// batch's flush points, authoritative list.</b> A
/// <see cref="WalkFrameEventKind.StreamMark"/> is NOT one of them: the
/// ordered stream it draws holds ONLY opaque batches — <c>WalkStaticStreamPopulator</c>'s
/// per-entity classify step appends to the stream iff <c>batch.IsOpaque</c>
/// and otherwise routes translucent batches to the alpha list
/// (WalkStaticStreamPopulator.cs:179) — drawn through the <c>Opaque</c>/
/// <c>OpaqueAlphaToCoverage</c> pipelines, both built with depth test AND
/// write ON and NO blending (<c>WbDrawDispatcher.CreateMeshPipeline</c>:
/// <c>Depth = new GpuDepthState(Test: true, Write: depthWrite,
/// depthCompare)</c> called with <c>depthWrite: true</c>,
/// <c>GpuBlendMode.None</c> for both, WbDrawDispatcher.Rhi.cs:240-248,343);
/// <c>DrawOrderedRange</c> resolves every walk-stream command's bucket to
/// <c>PipelineBucket.Opaque</c> because every command it holds is opaque
/// (WbDrawDispatcher.OrderedStream.cs:569-571). Opaque terrain and opaque
/// statics are therefore depth-resolved correctly in EITHER submission
/// order — pixel-identical except exact z-ties — so deferring the pending
/// terrain batch across a <c>StreamMark</c> changes nothing observable.
/// The flush points that DO remain, because their GPU order against
/// terrain IS observable: <see cref="WalkFrameEventKind.PunchFan"/>
/// (<c>DEPTHTEST_ALWAYS</c> + write — the far-Z stamp this chunk's
/// interleave exists to order correctly against nearer terrain, S3 §9.1
/// R4), <see cref="WalkFrameEventKind.AlphaBarrier"/> and
/// <see cref="WalkFrameEventKind.LandscapeFlush"/> (both drain the
/// translucent alpha list, which must see every opaque surface — terrain
/// included — already resolved in the depth buffer),
/// <see cref="WalkFrameEventKind.ClearInteriorDepth"/>,
/// <see cref="WalkFrameEventKind.ExitSeals"/>,
/// <see cref="WalkFrameEventKind.Sky"/>, <see cref="WalkFrameEventKind.CellShell"/>,
/// a <see cref="WalkFrameEventKind.StaticParticles"/>/
/// <see cref="WalkFrameEventKind.CellParticles"/> turn whose cell has a
/// renderable emitter (a genuinely empty turn still submits nothing and
/// still does not flush, unchanged from fix round 1's F2), and the end of
/// this method. This list's premise is the opaque-only, depth-test-and-
/// write, no-blend pipeline pair the walk's ordered stream actually uses
/// TODAY — not <c>StreamMark</c> itself: if any future ordered-stream
/// stage draws with blending or a disabled depth write (for example the
/// deferred building-detail overlay replay,
/// <c>WbDrawDispatcher.DrawBuildingDetailRangeRhi</c>'s
/// <c>RetailDetailTransparent</c> pass — not wired into this walk path
/// today, OrderedStream.cs:46-52), that stage's <c>StreamMark</c> MUST
/// become a flush point again; the contract gets amended, not bent.</para>
/// </summary>
internal void Replay(IGpuFrame frame, IGpuPassEncoder encoder)
{
@ -962,7 +1020,18 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
switch (e.Kind)
{
case WalkFrameEventKind.StreamMark:
FlushPendingTerrainBatch();
// S3 chunk 3 fix round 2 (F10): NOT a terrain flush
// point. The ordered stream this draws holds ONLY
// opaque batches (WalkStaticStreamPopulator.cs:179)
// drawn with the Opaque/OpaqueAlphaToCoverage
// pipelines — depth test AND write on, no blending
// (WbDrawDispatcher.Rhi.cs:240-248,343; bucket
// resolution WbDrawDispatcher.OrderedStream.cs:569-571)
// — so opaque terrain and opaque statics are
// depth-resolved correctly in either submission
// order. See this method's own doc comment for the
// full flush-point list and the citation this rests
// on.
int end = e.IntArg;
int count = end - cursor;
if (_trace is not null)
@ -971,12 +1040,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
cursor = end;
break;
case WalkFrameEventKind.AlphaSubmitMark:
// Not a flush point (F2): this arm only ENQUEUES the
// translucent instances into the CPU alpha list
// (SubmitWalkAlphaInstance); nothing reaches the GPU
// until the list drains at an AlphaBarrier /
// LandscapeFlush leaf or the pass end — each of which
// flushes the pending terrain batch first, so the
// Not a flush point (unchanged by fix round 2, F10):
// this arm only ENQUEUES the translucent instances
// into the CPU alpha list (SubmitWalkAlphaInstance);
// nothing reaches the GPU until the list drains at an
// AlphaBarrier / LandscapeFlush leaf or the pass end
// — both remain terrain flush points (F10) — so the
// drained alpha still lands after every preceding
// land cell's terrain, exactly as when unbatched.
int alphaEnd = e.IntArg;
@ -995,11 +1064,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_leafRenderer.DrawSky();
break;
case WalkFrameEventKind.LandCell:
// S3 chunk 3 fix round 1 (F2): append-only — no
// lookahead, no same-landblock restriction, no
// flush. The batch may span several landblocks; it
// flushes only when a later event actually needs the
// GPU (or at Replay's own end).
// S3 chunk 3 fix round 1 (F2), round 2 (F10):
// append-only — no lookahead, no same-landblock
// restriction, no flush. The batch may span several
// landblocks; it flushes only at one of the F10
// flush points (PunchFan, AlphaBarrier,
// LandscapeFlush, ClearInteriorDepth, ExitSeals,
// Sky, CellShell, a has-emitter particle turn) or at
// Replay's own end — NOT at a StreamMark, since the
// opaque ordered stream's GPU order against terrain
// is unobservable (see Replay's own doc comment).
_pendingTerrainBatch.Add((e.CellId, e.IntArg >> 8, e.IntArg & 0xFF));
break;
case WalkFrameEventKind.CellShell:
@ -1059,8 +1133,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
break;
}
}
// S3 chunk 3 fix round 1 (F2): the end of Replay flushes the
// remainder — a batch never crosses into the next frame.
// S3 chunk 3 fix round 1 (F2), round 2 (F10): the end of Replay
// is itself a flush point — the remainder never crosses into
// the next frame.
FlushPendingTerrainBatch();
}
finally
@ -1076,13 +1151,17 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
}
}
/// <summary>S3 chunk 3 fix round 1 (F2): submits the pending terrain
/// batch as ONE <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/>
/// <summary>S3 chunk 3 fix round 1 (F2), round 2 (F10): submits the
/// pending terrain batch as ONE <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/>
/// call, if it holds anything, and clears it. Called immediately before
/// every OTHER event kind that will itself submit GPU work — see
/// <see cref="Replay"/>'s own doc comment for the full flush-point
/// list — so the batch's actual submission point is always the exact
/// point the unbatched per-cell draws would have occupied.</summary>
/// every event kind whose GPU order against terrain is OBSERVABLE — see
/// <see cref="Replay"/>'s own doc comment for the full flush-point list
/// and its citation — so the batch's actual submission point is always
/// the exact point the unbatched per-cell draws would have occupied
/// relative to those events. A <see cref="WalkFrameEventKind.StreamMark"/>
/// is deliberately NOT one of the callers (F10): the opaque ordered
/// stream it draws is depth-resolved identically regardless of
/// submission order relative to terrain.</summary>
private void FlushPendingTerrainBatch()
{
if (_pendingTerrainBatch.Count == 0)