fix(render): S3 chunk 3 re-review follow-ups

- the LandCell event doc states the deferred cross-block batch rule (the
  same-landblock lookahead text was stale);
- the AlphaSubmitMark arm's comment gives the true reason it is not a
  flush point (it only enqueues into the CPU alpha list; the drain leaves
  flush first);
- the pending terrain batch is cleared with the other transient frame
  lists in AbortFrame/BeginFrame;
- CompleteWalkTerrainFrame runs in a finally so a throwing Replay cannot
  leak its accumulated ticks into the next frame's sample;
- CopyRenderableEmittersInCell keeps its own doc comment;
- the outdoor-root LandCell pin now observes one real cell turn after its
  terrain;
- the oh1 landscape contract's stale "whole pre-stage" prose is resolved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 10:00:28 +02:00
parent 651badc2b9
commit e10765aaa0
5 changed files with 58 additions and 39 deletions

View file

@ -588,14 +588,21 @@ internal sealed class RetailPViewRenderer
// filter) is gone — the walk's own per-cell LandCell turns are now
// the sole terrain-visibility authority, so there is no plumbing
// left to feed here.
driver.Replay(frame, encoder);
// S3 chunk 3 fix round 1 (F3): "the end of the walk replay" —
// exactly where the deleted whole-stage terrain leaf's own
// Begin()/Complete() bracket used to close. Pushes this frame's ONE
// accumulated terrain-timing sample (every DrawWalkLandCellBatch
// call between here and the last CompleteWalkTerrainFrame) and
// publishes the periodic [TERRAIN-DIAG] line if the cadence is due.
passes.CompleteWalkTerrainFrame();
try
{
driver.Replay(frame, encoder);
}
finally
{
// S3 chunk 3 fix round 1 (F3): "the end of the walk replay" —
// exactly where the deleted whole-stage terrain leaf's own
// Begin()/Complete() bracket used to close. Pushes this frame's
// ONE accumulated terrain-timing sample (every DrawWalkLandCellBatch
// call since the last CompleteWalkTerrainFrame) and publishes the
// periodic [TERRAIN-DIAG] line if the cadence is due. In a finally
// so a throwing Replay cannot leak its ticks into the next frame.
passes.CompleteWalkTerrainFrame();
}
// Landscape-stage static-owner particles (candles, the cathedral
// falls) submit AT THEIR OWN WALK TURNS inside Replay

View file

@ -282,15 +282,16 @@ internal enum WalkFrameEventKind : byte
/// <summary><see cref="IWalkFrameLeafRenderer.DrawSky"/>.</summary>
Sky,
/// <summary>S3 chunk 3: <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/>
/// for ONE admitted land cell — <see cref="WalkFrameEvent.CellId"/> is
/// the owning landblock id, <see cref="WalkFrameEvent.IntArg"/> packs
/// <c>(sideCellCount &lt;&lt; 8) | cellIndex</c> (side ∈ {1,2,4,8},
/// cellIndex &lt; side², both fit comfortably below the 8-bit shift).
/// <see cref="Replay"/> merges a run of consecutive same-landblock
/// entries into ONE <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/>
/// call (B2's order-preserving batching) rather than replaying them
/// one at a time.</summary>
/// <summary><see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/> —
/// one admitted land cell's terrain (retail <c>DrawLandCell</c>
/// @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>
LandCell,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
@ -741,6 +742,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0;
VisitedCells.Clear();
LookInCellTurns.Clear();
@ -877,6 +879,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0;
VisitedCells.Clear();
LookInCellTurns.Clear();
@ -968,12 +971,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
cursor = end;
break;
case WalkFrameEventKind.AlphaSubmitMark:
// Not a flush point (F2): an AlphaSubmitMark event is
// always recorded alongside — immediately after — a
// StreamMark from the SAME MarkIfGrown/MarkAlphaIfGrown
// pairing (every Collect-side call site pairs them),
// so the StreamMark case above already flushed any
// pending batch by the time this one runs.
// 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
// drained alpha still lands after every preceding
// land cell's terrain, exactly as when unbatched.
int alphaEnd = e.IntArg;
for (; alphaCursor < alphaEnd; alphaCursor++)
{
@ -1064,6 +1069,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0;
_readyToReplay = false;
_ctx = null;

View file

@ -606,17 +606,6 @@ public sealed class ParticleSystem : IParticleSystem
destination.Sort(static (left, right) => left.Handle.CompareTo(right.Handle));
}
/// <summary>
/// Copies the renderable emitters whose <see cref="ParticleEmitter.OwnerCellId"/>
/// is <paramref name="cellId"/>, retaining spawn order. Retail
/// <c>CPhysicsObj::add_particle_shadow_to_cell</c> (0x00514a70) gives an
/// emitter exactly one shadow in its own current cell, independent of its
/// attached owner's registry membership — a hidden/suspended owner's
/// emitter is still enumerated here as long as it remains renderable
/// (presentation-visible and view-eligible). No per-call allocation after
/// warmup: the bucket's own sorted handle list is copied through the
/// retained <see cref="_scopeHandleScratch"/> buffer.
/// </summary>
/// <summary>
/// S3 chunk 3 fix round 1 (F2): whether <paramref name="cellId"/> has any
/// renderable emitter for <paramref name="renderPass"/>, WITHOUT copying
@ -635,6 +624,17 @@ public sealed class ParticleSystem : IParticleSystem
&& bucket.FirstRenderableHandle != 0;
}
/// <summary>
/// Copies the renderable emitters whose <see cref="ParticleEmitter.OwnerCellId"/>
/// is <paramref name="cellId"/>, retaining spawn order. Retail
/// <c>CPhysicsObj::add_particle_shadow_to_cell</c> (0x00514a70) gives an
/// emitter exactly one shadow in its own current cell, independent of its
/// attached owner's registry membership — a hidden/suspended owner's
/// emitter is still enumerated here as long as it remains renderable
/// (presentation-visible and view-eligible). No per-call allocation after
/// warmup: the bucket's own sorted handle list is copied through the
/// retained <see cref="_scopeHandleScratch"/> buffer.
/// </summary>
public void CopyRenderableEmittersInCell(
ParticleRenderPass renderPass,
uint cellId,