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

@ -311,10 +311,11 @@ weather/sky post-pass
| Cell object draw consumes the cell's exact stable far-to-near `shadow_part_list`. | The driver materializes per-cell record buckets, classifies/batches them, and uses a scope-level ordered stream plus separate alpha collection. | OH3/OH7 must replace reconstructed membership/order with exact part entries and exact per-cell alpha barriers. | | Cell object draw consumes the cell's exact stable far-to-near `shadow_part_list`. | The driver materializes per-cell record buckets, classifies/batches them, and uses a scope-level ordered stream plus separate alpha collection. | OH3/OH7 must replace reconstructed membership/order with exact part entries and exact per-cell alpha barriers. |
| Building is alpha drain -> portal pass -> shell, then cell objects. | `RetailFrameWalk.DrawBuilding` and the driver's building events model this order. | Mechanism broadly matches, but its correctness depends on the sibling OH1 built-mesh/view and depth-lifecycle findings. | | Building is alpha drain -> portal pass -> shell, then cell objects. | `RetailFrameWalk.DrawBuilding` and the driver's building events model this order. | Mechanism broadly matches, but its correctness depends on the sibling OH1 built-mesh/view and depth-lifecycle findings. |
The architecture SSOT currently says that the walk records one retail-ordered The architecture SSOT says that the walk records one retail-ordered stream
stream including landscape cells, but the implementation and its own comments including landscape cells; until S3 chunk 3 the implementation drew terrain as
admit that terrain is one whole pre-stage. That is an architecture/code one whole pre-stage (an architecture/code contradiction, RESOLVED 2026-09-03:
contradiction, not a documented adaptation. The same architecture section `RetailFrameWalk.DrawLandscape` now emits one land-cell terrain turn per
admitted cell before that cell's own building/object turn). The same architecture section
also states that every admitted portal slice GPU-clips the mesh; the dirty also states that every admitted portal slice GPU-clips the mesh; the dirty
built-mesh candidate says the opposite. The sibling built-mesh evidence must built-mesh candidate says the opposite. The sibling built-mesh evidence must
arbitrate that claim before the architecture text or production path is arbitrate that claim before the architecture text or production path is

View file

@ -588,14 +588,21 @@ internal sealed class RetailPViewRenderer
// filter) is gone — the walk's own per-cell LandCell turns are now // filter) is gone — the walk's own per-cell LandCell turns are now
// the sole terrain-visibility authority, so there is no plumbing // the sole terrain-visibility authority, so there is no plumbing
// left to feed here. // left to feed here.
try
{
driver.Replay(frame, encoder); driver.Replay(frame, encoder);
}
finally
{
// S3 chunk 3 fix round 1 (F3): "the end of the walk replay" — // S3 chunk 3 fix round 1 (F3): "the end of the walk replay" —
// exactly where the deleted whole-stage terrain leaf's own // exactly where the deleted whole-stage terrain leaf's own
// Begin()/Complete() bracket used to close. Pushes this frame's ONE // Begin()/Complete() bracket used to close. Pushes this frame's
// accumulated terrain-timing sample (every DrawWalkLandCellBatch // ONE accumulated terrain-timing sample (every DrawWalkLandCellBatch
// call between here and the last CompleteWalkTerrainFrame) and // call since the last CompleteWalkTerrainFrame) and publishes the
// publishes the periodic [TERRAIN-DIAG] line if the cadence is due. // 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(); passes.CompleteWalkTerrainFrame();
}
// Landscape-stage static-owner particles (candles, the cathedral // Landscape-stage static-owner particles (candles, the cathedral
// falls) submit AT THEIR OWN WALK TURNS inside Replay // 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> /// <summary><see cref="IWalkFrameLeafRenderer.DrawSky"/>.</summary>
Sky, Sky,
/// <summary>S3 chunk 3: <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/> /// <summary><see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/> —
/// for ONE admitted land cell — <see cref="WalkFrameEvent.CellId"/> is /// one admitted land cell's terrain (retail <c>DrawLandCell</c>
/// the owning landblock id, <see cref="WalkFrameEvent.IntArg"/> packs /// @0x0059f120): <see cref="WalkFrameEvent.CellId"/> is the landblock,
/// <c>(sideCellCount &lt;&lt; 8) | cellIndex</c> (side ∈ {1,2,4,8}, /// <see cref="WalkFrameEvent.IntArg"/> packs <c>side &lt;&lt; 8 | cellIndex</c>.
/// cellIndex &lt; side², both fit comfortably below the 8-bit shift). /// <see cref="Replay"/> does NOT draw it immediately: it appends the
/// <see cref="Replay"/> merges a run of consecutive same-landblock /// cell to ONE pending terrain batch that may span landblocks and flushes
/// entries into ONE <see cref="IWalkFrameLeafRenderer.DrawLandCellBatch"/> /// that batch only before a leaf that will submit GPU work (S3 chunk 3
/// call (B2's order-preserving batching) rather than replaying them /// fix round 1, §9.6 F2) — a particle turn whose cell has no renderable
/// one at a time.</summary> /// emitter submits nothing and keeps the batch open. The GPU submission
/// order is therefore identical to replaying each cell alone.</summary>
LandCell, LandCell,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> — /// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
@ -741,6 +742,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_events.Clear(); _events.Clear();
_markPositions.Clear(); _markPositions.Clear();
_alphaSubmissions.Clear(); _alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0; _alphaSubmitMark = 0;
VisitedCells.Clear(); VisitedCells.Clear();
LookInCellTurns.Clear(); LookInCellTurns.Clear();
@ -877,6 +879,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_events.Clear(); _events.Clear();
_markPositions.Clear(); _markPositions.Clear();
_alphaSubmissions.Clear(); _alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0; _alphaSubmitMark = 0;
VisitedCells.Clear(); VisitedCells.Clear();
LookInCellTurns.Clear(); LookInCellTurns.Clear();
@ -968,12 +971,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
cursor = end; cursor = end;
break; break;
case WalkFrameEventKind.AlphaSubmitMark: case WalkFrameEventKind.AlphaSubmitMark:
// Not a flush point (F2): an AlphaSubmitMark event is // Not a flush point (F2): this arm only ENQUEUES the
// always recorded alongside — immediately after — a // translucent instances into the CPU alpha list
// StreamMark from the SAME MarkIfGrown/MarkAlphaIfGrown // (SubmitWalkAlphaInstance); nothing reaches the GPU
// pairing (every Collect-side call site pairs them), // until the list drains at an AlphaBarrier /
// so the StreamMark case above already flushed any // LandscapeFlush leaf or the pass end — each of which
// pending batch by the time this one runs. // 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; int alphaEnd = e.IntArg;
for (; alphaCursor < alphaEnd; alphaCursor++) for (; alphaCursor < alphaEnd; alphaCursor++)
{ {
@ -1064,6 +1069,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_events.Clear(); _events.Clear();
_markPositions.Clear(); _markPositions.Clear();
_alphaSubmissions.Clear(); _alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0; _alphaSubmitMark = 0;
_readyToReplay = false; _readyToReplay = false;
_ctx = null; _ctx = null;

View file

@ -606,17 +606,6 @@ public sealed class ParticleSystem : IParticleSystem
destination.Sort(static (left, right) => left.Handle.CompareTo(right.Handle)); 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> /// <summary>
/// S3 chunk 3 fix round 1 (F2): whether <paramref name="cellId"/> has any /// S3 chunk 3 fix round 1 (F2): whether <paramref name="cellId"/> has any
/// renderable emitter for <paramref name="renderPass"/>, WITHOUT copying /// renderable emitter for <paramref name="renderPass"/>, WITHOUT copying
@ -635,6 +624,17 @@ public sealed class ParticleSystem : IParticleSystem
&& bucket.FirstRenderableHandle != 0; && 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( public void CopyRenderableEmittersInCell(
ParticleRenderPass renderPass, ParticleRenderPass renderPass,
uint cellId, uint cellId,

View file

@ -1360,7 +1360,12 @@ public sealed class WalkFrameDriverTests
using var fx = new DispatcherFixture(); using var fx = new DispatcherFixture();
var log = new List<string>(); var log = new List<string>();
var leaf = new RecordingLeafRenderer(log); var leaf = new RecordingLeafRenderer(log);
// Every covered bucket but ONE reports no renderable emitter, so the
// block's object turns submit nothing (no flush) except bucket
// 0xF4180001, whose particle turn is a real submission — it must land
// AFTER the coarse cell's own terrain (retail DrawLandCell → DrawSortCell).
leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u)); leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u));
leaf.CellsWithoutEmitters.Remove(0xF4180001u);
var ctx = new TestContext(); var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData()); var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
var walk = new RetailFrameWalk(); var walk = new RetailFrameWalk();
@ -1378,7 +1383,7 @@ public sealed class WalkFrameDriverTests
driver.EndFrame(); driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass); driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { "SKY", "LANDCELL:f4180000:1:0" }, log); Assert.Equal(new[] { "SKY", "LANDCELL:f4180000:1:0", "PARTICLES:f4180001" }, log);
Assert.Equal(new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xF4180000u, 1, 0) }, Assert.Equal(new (uint LandblockId, int SideCellCount, int CellIndex)[] { (0xF4180000u, 1, 0) },
Assert.Single(leaf.LandCellBatches)); Assert.Single(leaf.LandCellBatches));
} }