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:
parent
0b770afc11
commit
896c63fe9a
2 changed files with 206 additions and 56 deletions
|
|
@ -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 << 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue