acdream/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs
Erik 74ac7aa227 fix(render): S3 chunk 3 round 2 follow-up — the StreamMark pin now fires real StreamMarks
The re-expressed F4(c) pin installed an empty landscape view, so nothing
classified visible, the ordered stream never grew, and no StreamMark
fired: the one behavior round 2 changes was unpinned (review finding).
The test now installs a real full-viewport view and records the trace,
asserting two FLUSH lines ahead of the merged three-cell terrain batch.
The LandCell doc no longer claims an identical submission order; the
resolved image is identical (opaque-vs-opaque order is depth-resolved,
every observable order still flushes first).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 10:44:55 +02:00

2008 lines
96 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW stage FW3.2b-1: one cell's or one building's already-queried
/// static content, ready for <see cref="WalkStaticStreamPopulator"/> —
/// caller-built, never read from a retained scene (see
/// <see cref="IWalkFrameWorldData"/>'s doc comment).
/// </summary>
/// <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). 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(
ArraySegment<RenderProjectionRecord> Records, uint TupleLandblockId)
{
public static readonly WalkFrameStaticRecords Empty =
new(ArraySegment<RenderProjectionRecord>.Empty, 0);
}
/// <summary>
/// Campaign FW stage FW3.2b-1: the world-data lookups <see cref="WalkFrameDriver"/>
/// needs at each walk turn, entirely caller-built — the driver reads no
/// retained scene state of its own (mirrors <see cref="WalkStaticStreamPopulator"/>'s
/// own "reads no retained scene state itself" contract one layer up). FW3.2b-2
/// wires the real production implementation (<c>RenderSceneQuery.CopyCellStaticsTo</c>
/// / <c>CopyIndexTo</c> + <see cref="WalkBuildingRegistry"/>); this stage's
/// headless referee tests wire a synthetic fake instead.
/// </summary>
internal interface IWalkFrameWorldData
{
/// <summary>An indoor <c>PView::DrawCells</c> flood cell's (or a building
/// look-in's) static content — <c>RenderProjectionClass.IndoorCellStatic</c>.</summary>
WalkFrameStaticRecords GetCellStatics(uint cellId);
/// <summary>The live dynamic occupants of one indoor cell. These records
/// enter the ordered stream at that cell's own object-list turn; a
/// building look-in supplies its exact installed portal view for the
/// per-part <c>viewconeCheck</c>.</summary>
WalkFrameStaticRecords GetCellDynamics(uint cellId);
/// <summary>One visited landscape (outdoor) cell's static content —
/// <c>RenderProjectionClass.OutdoorStatic</c>, keyed by the SAME
/// <c>(landblockId &amp; 0xFFFF0000) | (cellIndex+1)</c> id
/// <see cref="IWalkEventSink.OnLandscapeCellTurn"/> computes.</summary>
WalkFrameStaticRecords GetOutdoorStatics(uint cellId);
/// <summary>One visited landscape cell's live dynamic occupants. The
/// walk draws these at the same <c>DrawObjCell</c> turn as its statics,
/// through the landscape's currently installed view set.</summary>
WalkFrameStaticRecords GetOutdoorDynamics(uint cellId);
/// <summary>One building's own exterior shell content (<c>IsBuildingShell</c>
/// records anchored at the building's position cell).</summary>
WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building);
/// <summary>Building-local → world, for transforming a punch polygon
/// before <see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> — the
/// production implementation is <see cref="WalkBuildingRegistry.TryGetEntry"/>'s
/// <c>WorldTransform</c> (FW3.2b-2 wiring).</summary>
Matrix4x4 GetBuildingWorldTransform(WalkBuilding building);
}
/// <summary>
/// Campaign FW stage FW3.2b-1: the leaf GPU-adjacent actions
/// <see cref="WalkFrameDriver"/> calls at walk turns that have no
/// <see cref="OrderedDrawStream"/> submission path YET (sky, terrain, an
/// EnvCell shell, a portal punch fan, the interior depth clear, the exit-
/// portal seals) or that aren't a draw at all (the
/// <see cref="RetailAlphaQueue"/> barrier). Kept as its own seam — rather
/// than folding these into <see cref="WalkFrameDriver"/> directly — so the
/// FW3.2b-1 headless referee suite can wire a fake and prove turn ORDER
/// without standing up the real renderers <c>EnvCellRenderer</c>,
/// <c>TerrainModernRenderer</c>, <c>GameSky</c>, and
/// <c>PortalDepthMaskRenderer.DrawDepthFan</c> — FW3.2b-2's job.
///
/// <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
{
/// <summary><c>LScape::draw</c> draws <c>GameSky</c> once per outdoor
/// walk (retail draws it once inside <c>LScape::draw</c>; the CURRENT
/// executor's per-slice call is per-slice-equals-once only for the
/// single-view outdoor case it handles today — the walk driver always
/// calls this exactly once per frame's Landscape turn).</summary>
void DrawSky();
/// <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
/// 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
/// (immediately before the next real submission), so pixel output is
/// identical to submitting each entry as its own draw. <paramref
/// name="cells"/> carries (landblockId, side, cellIndex) triples in
/// event (draw) order — the R3 index-run arithmetic (contiguous runs
/// inside each entry's own landblock's 384-index slot,
/// <c>TerrainModernRenderer.AppendCellIndexRuns</c>) runs per entry.
/// Draws UNCLIPPED for both root kinds: retail never view-clips terrain
/// (<c>LScape::draw</c> draws whole blocks; the exit-seal/interior-
/// repaint turn owns aperture exactness afterward, S3 §9.1 R4/R5).</summary>
void DrawLandCellBatch(
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells);
/// <summary>S3 chunk 3 fix round 1 (F2): whether <paramref
/// name="cellId"/> has any renderable emitter right now — production
/// wires this to <c>ParticleSystem.HasRenderableEmittersInCell</c> with
/// the SAME <c>ParticleRenderPass.Scene</c> pass both <see
/// cref="DrawStaticParticles"/> and <see cref="DrawCellParticles"/>
/// already draw through. <see cref="WalkFrameDriver.Replay"/> asks this
/// BEFORE flushing the pending terrain batch (or calling either particle
/// leaf) so a genuinely empty particle turn submits nothing and does not
/// force an otherwise-unneeded terrain flush.</summary>
bool HasRenderableEmittersInCell(uint cellId);
/// <summary>One committed cell's EnvCell shell —
/// <c>PView::DrawCells</c>'s <c>DrawEnvCell</c> @0x005a4abe. Retail first
/// draws ALL shells in reverse <c>cell_draw_list</c> order, then starts a
/// second reverse loop for <c>DrawObjCellForDummies</c> @0x005a4b0d.
/// The ordinary interior-root flood and every building look-in flood use
/// this same two-pass discipline. Retail's runtime <c>use_built_mesh</c>
/// branch submits the complete constructed shell; the cell's drawn stamp,
/// portal depth writes, and later depth-tested repaint own visibility.</summary>
void DrawCellShell(uint cellId);
/// <summary>One landscape (land) cell's own-cell particle emitters, at
/// its own walk turn — see <see cref="WalkFrameEventKind.StaticParticles"/>
/// for the retail positional invariant this carries (the #132 falls
/// containment). Retail <c>CPhysicsObj::add_particle_shadow_to_cell</c>
/// (0x00514a70) draws an emitter at its own current cell's object turn
/// regardless of its attached owner's registry membership, so this is a
/// cell lookup, not an owner union.</summary>
void DrawStaticParticles(uint cellId);
/// <summary>Submits one indoor cell's own-cell particle emitters at that
/// cell's own object-list turn. Meshes have already entered the ordered
/// stream before this leaf event.</summary>
void DrawCellParticles(uint cellId);
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
/// (pc:432731-432732) between the outside stage and the interior root's
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
/// (see that interface's own member of the same name in
/// <c>RetailPViewRenderer.cs</c>). The driver calls this leaf ONLY when
/// <see cref="WalkFrameDriver.PortalsDrawnCount"/> was nonzero at the
/// read-then-zero decision point (S3 §8.1 R4: retail's
/// <c>portalsDrawnCount</c>, read-then-zeroed @0x005a489c-0x005a489e —
/// <c>forceClear</c> never writes in the pseudo-C, so the clear fires
/// iff the counter was nonzero). Only called for an INTERIOR root, never
/// outdoors (retail has no depth clear there —
/// <c>portalsDrawnCount</c> never applies to <c>LScape::draw</c>'s own
/// top-level walk) and never for a building look-in (R1: those call
/// <c>DrawCells</c> re-entrantly with <c>ov==0</c>, which never reaches
/// this leaf at all).</summary>
void ClearInteriorDepth();
/// <summary>The exit-portal seals (pc:432785-432786) — re-stamping every
/// outside-leading portal's TRUE depth right after
/// <see cref="ClearInteriorDepth"/> (when it ran) so the aperture the
/// clear just wiped stays occluded by the world beyond it rather than by
/// whatever draws next. Production maps this to the existing seal-fan
/// machinery (<c>RetailPViewRenderer.DrawExitPortalMask</c>/
/// <c>PortalDepthMaskRenderer</c>) — this driver only provides the TURN;
/// the real per-portal fan geometry is FW3.2b-2's job. Returns the
/// number of seal polygons actually submitted this turn (S3 §8.1 R4/§8.2
/// B2: retail's <c>D3DPolyRender::portalsDrawnCount</c> @0x008719b4
/// increments once per <c>DrawPortalPolyInternal</c> call with its
/// second argument FALSE — exactly the exit-seal calls, never punch
/// fans) — the driver adds the returned count to
/// <see cref="WalkFrameDriver.PortalsDrawnCount"/>, which the NEXT
/// <c>ov&gt;0</c> interior-root flood's <see cref="ClearInteriorDepth"/>
/// decision reads. Only called for an INTERIOR root's own flood, never
/// for a building look-in (those call <c>DrawCells</c> re-entrantly with
/// no clear/seal step) and never outdoors.</summary>
int DrawExitSeals();
/// <summary>Retail <c>D3DPolyRender::FlushAlphaList(0f)</c> @0x005a4872
/// plus the pre-clear dynamics hook (<c>RetailPViewRenderer</c>'s
/// <c>FlushWalkLandscape</c>) — the first action inside
/// <c>if (outside_view.view_count &gt; 0)</c> at the interior root's own
/// flood turn (S3 §8.1 R3), strictly before the device-stamp advance and
/// the gated depth clear. Distinct from <see cref="AlphaBarrier"/> (a
/// BUILDING turn's own alpha barrier, @0x0059f30b) even though both map
/// to the same underlying flush call — kept as a separate leaf member so
/// a driver trace can tell the two turns apart. Only called for an
/// INTERIOR root's own flood with a surviving exit view (<c>ov&gt;0</c>),
/// never outdoors and never for a building look-in.</summary>
void FlushLandscape();
/// <summary><c>DrawPortalPolyInternal</c> @0x0059bc90's depth-only far-Z
/// 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"/> 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) — 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>Retail <c>D3DPolyRender::FlushAlphaList(0f)</c>
/// @0x0059f30b. The walk is positional: only farther cells have submitted
/// alpha when a building turn fires, so this is a full drain with no
/// synthetic viewer-distance threshold.</summary>
void AlphaBarrier();
}
/// <summary>
/// Campaign FW stage FW3.2b-1 test seam: an optional, diagnostic-only
/// 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 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>Submits every retained translucent walk batch through
/// <see cref="WbDrawDispatcher.SubmitWalkAlphaInstance"/> up to the
/// exclusive-end index in <see cref="WalkFrameEvent.IntArg"/>. This is a
/// replay-time event: collecting the complete frame must not make alpha
/// from a later/near cell visible to an earlier building barrier.</summary>
AlphaSubmitMark,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawSky"/>.</summary>
Sky,
/// <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 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 resolved image is therefore identical to replaying each cell
/// alone: every event whose GPU order against opaque terrain is
/// observable still flushes first (§9.7 F10), and opaque-vs-opaque
/// order is depth-resolved.</summary>
LandCell,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
/// <see cref="WalkFrameEvent.CellId"/> is the cell. Retail's
/// <c>DrawEnvCell</c> marks the EnvCell drawn before submitting its built
/// mesh, so the shell is whole and frame-deduplicated; portal-view slices
/// are not shell geometry clips.</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.FlushLandscape"/>.</summary>
LandscapeFlush,
/// <summary><see cref="IWalkFrameLeafRenderer.ClearInteriorDepth"/>.</summary>
ClearInteriorDepth,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawExitSeals"/>.</summary>
ExitSeals,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawStaticParticles"/> —
/// ONE landscape (land) cell's (<see cref="WalkFrameEvent.CellId"/>)
/// own-cell particle emitters, emitted AT ITS OWN WALK TURN. Retail's
/// falls containment is positional: an outdoor emitter's polys join the
/// one alpha list during its owner cell's <c>DrawObjCell</c> in the
/// far-to-near landscape walk, so every nearer building's pre-punch
/// alpha barrier (<c>DrawBuilding</c> @0x0059f2a0's
/// <c>FlushAlphaList</c> @0x0059f30b) drains the already-queued FARTHER
/// content against still-true depth BEFORE the punch stamps far-Z into
/// the aperture. The former single batched submission at the pre-clear
/// closure ran AFTER every punch — the barriers fired over an empty
/// queue and the falls drained against punched-far aperture pixels (the
/// cathedral bleed; the old pipeline's user-verified #132 fix
/// `e102fb36` encoded the same invariant). A building's own shell fires
/// no event of this kind any more — its interior emitters live in their
/// own EnvCells and draw at those cells' own <see cref="CellParticles"/>
/// turns (Campaign OVERHAUL S2 chunk 6, retail
/// <c>CPhysicsObj::add_particle_shadow_to_cell</c> 0x00514a70: an emitter
/// owns one shadow in its OWN current cell, never its parent's).</summary>
StaticParticles,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellParticles"/> —
/// <see cref="WalkFrameEvent.CellId"/> is the cell. Fires at EVERY
/// visited interior cell's own object-list turn regardless of whether
/// that cell has any visible static/dynamic owner record: an emitter
/// draws at its own cell's turn independent of its attached owner's
/// registry membership (a hidden/suspended owner's emitter still shows —
/// Campaign OVERHAUL S2 chunk 6, the portal-haze fix).</summary>
CellParticles,
}
/// <summary>
/// The exact portal-view cones installed at the walk's building look-in
/// <c>DrawCells</c> turns. Retail <c>RenderDeviceD3D::DrawMesh</c>
/// @0x005A0860 tests each object's drawing sphere against these views before
/// drawing the mesh whole; cell membership alone is not an admission rule.
/// </summary>
internal interface IWalkLookInViewSource
{
IReadOnlyList<uint> LookInCellTurns { get; }
bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius);
/// <summary>Returns the GPU clip slots for every view slice in this
/// DrawObjCell turn which admits the part's retail drawing sphere. Retail
/// runs DrawMeshInternal once per surviving portal_view slice; a boolean
/// sphere gate alone is insufficient because the mesh polygons must remain
/// clipped to that aperture.</summary>
IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
int routeIndex,
in Vector3 center,
float radius,
bool testSphere);
/// <summary>Output-only description of the exact CY/edge-plane distances
/// used by one DrawObjCell turn. Kept behind the existing Facility probe;
/// production admission never consumes this text.</summary>
string DescribeLookInTurn(
int routeIndex,
in Vector3 center,
float radius) => "unavailable";
}
internal readonly record struct WalkLookInSlice(
int PlaneStart,
int PlaneCount,
uint ClipSlot);
internal readonly record struct WalkLookInTurn(
uint CellId,
int SliceStart,
int SliceCount);
/// <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&lt;WalkFrameEvent&gt;</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 AlphaSubmitMark(int exclusiveEnd) =>
new(WalkFrameEventKind.AlphaSubmitMark, exclusiveEnd, 0, 0f, null);
internal static WalkFrameEvent Sky() =>
new(WalkFrameEventKind.Sky, 0, 0, 0f, null);
internal static WalkFrameEvent LandCell(uint landblockId, int sideCellCount, int cellIndex) =>
new(WalkFrameEventKind.LandCell, (sideCellCount << 8) | cellIndex, landblockId, 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() =>
new(WalkFrameEventKind.AlphaBarrier, 0, 0, 0f, null);
internal static WalkFrameEvent LandscapeCellParticles(uint cellId) =>
new(WalkFrameEventKind.StaticParticles, 0, cellId, 0f, null);
internal static WalkFrameEvent CellParticles(uint cellId) =>
new(WalkFrameEventKind.CellParticles, 0, cellId, 0f, null);
internal static WalkFrameEvent LandscapeFlush() =>
new(WalkFrameEventKind.LandscapeFlush, 0, 0, 0f, 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"/>, so that GPU command-buffer order
/// equals retail's walk order (plan §FW3.2b-1's "INTERLEAVING RULE").
///
/// <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>LandCell</c>, <c>CellShell</c>, <c>LandscapeFlush</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 retail's two reverse
/// flood passes (ALL shells, then ALL contents),
/// 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 → flush/stamp/[gated clear]/seals
/// → the flood's own cells, the middle four steps ALL gated on
/// <c>outside_view.view_count &gt; 0</c> — 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: [far shell] … [near shell]
/// [far contents] … [near contents], [alpha barrier] [punch fan(s) +
/// look-in flood(s), each following the SAME reverse two-pass discipline]
/// [building shell content mark], [landscape (if exit views survived)]
/// [landscape flush] [gated interior depth clear] [exit-portal seals] — the
/// landscape and those three only when <c>outside_view.view_count &gt; 0</c>
/// (S3 §8.1 R3) — then [the interior root's own flood cells] unconditionally,
/// 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),
/// <c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140 (building-before-
/// DrawObjCell per landscape cell), <c>PView::DrawCells</c> @0x005a4840
/// (the complete reverse <c>DrawEnvCell</c> loop @0x005a4a000x005a4ade
/// before the complete reverse <c>DrawObjCellForDummies</c> loop
/// @0x005a4ade0x005a4b2d; <c>LScape::draw</c> pc:432719, the depth
/// clear pc:432731-432732, and the exit-portal seals pc:432785-432786 —
/// ALL strictly before the flood's own cells, though the event marking
/// <c>DrawCells</c> entry fires before all three), <c>RenderDeviceD3D::DrawBuilding</c>
/// @0x0059f2a0 (the <c>part-&gt;gfxobj[deg_level]!=0</c> gate @0x0059f2d3
/// and the alpha-barrier → portal-pass → shell order @0x0059f30b0x0059f345).</para>
/// </summary>
internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
{
private readonly WbDrawDispatcher _dispatcher;
private readonly WalkStaticStreamPopulator _populator;
private IWalkFrameLeafRenderer _leafRenderer;
private readonly IWalkFrameWorldData _worldData;
private readonly IWalkFrameDriverTrace? _trace;
private ClipFrame? _clipFrame;
private readonly OrderedDrawStream _stream = new();
private readonly List<WalkFrameEvent> _events = new();
private readonly List<int> _markPositions = new();
private readonly List<WbDrawDispatcher.WalkClassifiedBatch> _alphaSubmissions = new();
private int _alphaSubmitMark;
// 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();
/// <summary>Building look-in cells in the exact order the walk encountered
/// their <c>DrawCells</c> turns. Duplicates are intentional: two authored
/// portal views can independently visit the same cell and therefore own
/// distinct packed route indices.</summary>
internal List<uint> LookInCellTurns { get; } = new();
private readonly List<WalkLookInTurn> _lookInTurns = new();
private readonly List<WalkLookInSlice> _lookInSlices = new();
private readonly List<WalkPlane> _lookInPlanes = new();
private readonly List<uint> _visibleClipSlotScratch = new();
private readonly List<int> _floodViewRouteScratch = new();
private WalkPlane _lookInCyPlane;
/// <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();
// Chunk 6 review F1: one particle turn per cell per render stamp (see EmitCellContentsTurn).
private readonly HashSet<uint> _cellParticleTurnsDrawnThisFrame = new();
/// <summary>S3 chunk 2 (§8.2 B2): retail's <c>D3DPolyRender::portalsDrawnCount</c>
/// (uint16 @0x008719b4) — retained ACROSS frames (this driver is itself
/// retained by <c>RetailPViewRenderer</c>), never cleared by
/// <see cref="BeginFrame"/>/<see cref="AbortFrame"/>/<see cref="EndFrame"/>/
/// <see cref="Replay"/>. Read-then-zeroed at every <c>ov&gt;0</c>
/// interior-root flood's clear decision (<see cref="OnInteriorFloodDrawTurn"/>
/// implementation, S3 §8.1 R4); incremented at <see cref="Replay"/> by
/// the count <see cref="IWalkFrameLeafRenderer.DrawExitSeals"/> returns
/// for THIS frame's own exit-seal turn, which the NEXT <c>ov&gt;0</c>
/// frame's read will see. A driver whose interior floods never reach an
/// exit portal keeps this at zero forever — the gated clear never fires
/// for it, matching retail exactly (no seals submitted, no depth ever
/// needed clearing). Retail consumes and re-arms inside ONE synchronous
/// <c>DrawCells</c>; here the consume is at Collect and the re-arm at
/// Replay, so a frame that Collects but never Replays (an exception path
/// that already failed the frame) leaves the next ov&gt;0 frame without a
/// clear — no retail analogue, no production path.</summary>
internal int PortalsDrawnCount;
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
/// exclusion and root-flood particle bookkeeping.</summary>
internal HashSet<uint> LookInCells { get; } = new();
/// <summary>FW4 slice 2: the interior root's ORDERED flood cell list,
/// exactly as retail's <c>PView::DrawCells</c> iterates it for the
/// exit-portal seals (pc:432785-432786) — captured at
/// <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>. Empty for an
/// outdoor-rooted frame. The production seal draw iterates THIS list
/// (the walk's own flood), not the old visibility apparatus's — at the
/// #456 seam band the old builder's flood misses exit portals the walk
/// provably floods, leaving unsealed aperture depth the end-of-frame
/// alpha drain splats through (the cathedral falls shine-through).</summary>
internal List<uint> InteriorFloodCells { get; } = new();
/// <summary>Number of exact walk-owned portal-view slices that retail's
/// <c>CEnvCell::setup_view</c> installs while sealing one entry in
/// <see cref="InteriorFloodCells"/>. The matching route was captured at
/// that cell's shell turn and remains valid through synchronous replay.</summary>
internal int InteriorFloodViewSliceCountAt(int floodCellIndex)
{
int routeIndex = InteriorFloodViewRouteAt(floodCellIndex);
return _lookInTurns[routeIndex].SliceCount;
}
/// <summary>Borrows the clip-space half-planes for one exact walk-owned
/// portal-view slice. An empty span is the legitimate no-clip slot, not a
/// fallback to another visibility structure.</summary>
internal ReadOnlySpan<Vector4> InteriorFloodViewClipPlanesAt(
int floodCellIndex,
int sliceOffset)
{
int routeIndex = InteriorFloodViewRouteAt(floodCellIndex);
WalkLookInTurn turn = _lookInTurns[routeIndex];
if ((uint)sliceOffset >= (uint)turn.SliceCount)
throw new ArgumentOutOfRangeException(nameof(sliceOffset));
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
if (_clipFrame is null)
return ReadOnlySpan<Vector4>.Empty;
return _clipFrame.GetSlotPlanes(slice.ClipSlot);
}
private int InteriorFloodViewRouteAt(int floodCellIndex)
{
if ((uint)floodCellIndex >= (uint)InteriorFloodCells.Count
|| (uint)floodCellIndex >= (uint)_floodViewRouteScratch.Count)
{
throw new ArgumentOutOfRangeException(nameof(floodCellIndex));
}
int routeIndex = _floodViewRouteScratch[floodCellIndex];
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
{
throw new InvalidOperationException(
$"Interior flood cell 0x{InteriorFloodCells[floodCellIndex]:X8} "
+ "has no captured portal-view route.");
}
return routeIndex;
}
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 Matrix4x4 _viewProjection;
private Vector3 _cameraWorldPosition;
/// <summary>Frame-scoped fail-loud guard (replaces the deleted
/// <c>_skyDrawnThisFrame</c> proxy — S3 chunk 2): counts this frame's
/// <see cref="WalkEventKind.Landscape"/> turns so a second one throws
/// (see <see cref="HandleLandscapeTurn"/>) rather than double-drawing
/// sky/terrain. Retail's own gate — the interior-root flood's
/// landscape/flush/stamp/clear/seal turn — now reads the real
/// <c>outside_view.view_count</c> the sink receives, not this counter.</summary>
private int _landscapeTurnsThisFrame;
private WalkDrawStage? _currentDcStage;
private bool _readyToReplay;
private int _cellViewRouteIndex;
private int _landscapeViewRouteIndex;
internal WalkFrameDriver(
WbDrawDispatcher dispatcher,
IWalkFrameLeafRenderer leafRenderer,
IWalkFrameWorldData worldData,
IWalkFrameDriverTrace? trace = null,
ClipFrame? clipFrame = null)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_leafRenderer = leafRenderer ?? throw new ArgumentNullException(nameof(leafRenderer));
_worldData = worldData ?? throw new ArgumentNullException(nameof(worldData));
_trace = trace;
_clipFrame = clipFrame;
_populator = new WalkStaticStreamPopulator(dispatcher);
}
/// <summary>
/// FW6 allocation closeout: reuse this driver's large event/view/stream
/// scratch across production frames while rebinding the frame-local leaf
/// and clip destination. A prior failed frame is discarded here so the
/// renderer's report-and-continue policy cannot poison the next frame.
/// </summary>
internal void RebindFrame(
IWalkFrameLeafRenderer leafRenderer,
ClipFrame? clipFrame)
{
AbortFrame();
_leafRenderer = leafRenderer
?? throw new ArgumentNullException(nameof(leafRenderer));
_clipFrame = clipFrame;
}
/// <summary>Discard only transient frame state; retained capacities stay
/// available for the next frame.</summary>
internal void AbortFrame()
{
_ctx = null;
_viewProjection = default;
_cameraWorldPosition = default;
_landscapeTurnsThisFrame = 0;
_currentDcStage = null;
_readyToReplay = false;
_stream.Reset();
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0;
VisitedCells.Clear();
LookInCellTurns.Clear();
_lookInTurns.Clear();
_lookInSlices.Clear();
_lookInPlanes.Clear();
_visibleClipSlotScratch.Clear();
_floodViewRouteScratch.Clear();
_dispatcher.EndWalkPartFrame();
_cellShellsDrawnThisFrame.Clear();
_cellParticleTurnsDrawnThisFrame.Clear();
_lookInCyPlane = default;
LookInCells.Clear();
VisitedBuildings.Clear();
VisitedLandscapeCellIds.Clear();
InteriorFloodCells.Clear();
_cellViewRouteIndex = 0;
_landscapeViewRouteIndex = -1;
}
/// <summary>
/// Copies the walk's complete retail <c>CObjCell::IsInView</c> answer:
/// EnvCells reached by interior floods/look-ins plus outdoor land cells
/// visited by the landscape walk. The two families stay separately
/// retained because only the first is valid EnvCell batch input, but
/// particles, lights, and shadows consume their union.
/// </summary>
internal void CopyVisibleCellsTo(HashSet<uint> destination)
{
ArgumentNullException.ThrowIfNull(destination);
if (ReferenceEquals(destination, VisitedCells)
|| ReferenceEquals(destination, VisitedLandscapeCellIds))
{
throw new ArgumentException(
"The visible-cell destination cannot alias a walk source set.",
nameof(destination));
}
destination.Clear();
destination.UnionWith(VisitedCells);
destination.UnionWith(VisitedLandscapeCellIds);
}
/// <summary>
/// Drives one complete frame at retail's root (<c>SmartBox::RenderNormalMode</c>):
/// <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,
uint cameraCellId,
WalkCell? cameraCell,
WalkLandscape landscape,
IRetailFrameWalkContext ctx,
IGpuFrame frame,
IGpuPassEncoder encoder,
Matrix4x4 viewProjection,
Vector3 cameraWorldPosition)
{
ArgumentNullException.ThrowIfNull(frame);
ArgumentNullException.ThrowIfNull(encoder);
Collect(
walk, cameraCellId, cameraCell, landscape, ctx,
viewProjection, cameraWorldPosition);
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)
{
ArgumentNullException.ThrowIfNull(walk);
ArgumentNullException.ThrowIfNull(landscape);
ArgumentNullException.ThrowIfNull(ctx);
BeginFrame(ctx, viewProjection, cameraWorldPosition);
try
{
walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this);
EndFrame();
}
catch
{
AbortFrame();
throw;
}
}
/// <summary>
/// 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="Collect"/> is implemented in terms of this pair. Performs no
/// GPU work — see this type's own doc comment.
/// </summary>
internal void BeginFrame(
IRetailFrameWalkContext ctx,
Matrix4x4 viewProjection,
Vector3 cameraWorldPosition)
{
ArgumentNullException.ThrowIfNull(ctx);
if (_ctx is not null)
{
throw new InvalidOperationException(
"WalkFrameDriver.BeginFrame was called while a previous frame was still open — "
+ "the driver is not re-entrant (Campaign FW3.2b-1 fail-loud rule); call "
+ "EndFrame (or let a thrown exception's cleanup run) before starting the next.");
}
_dispatcher.BeginWalkPartFrame();
_ctx = ctx;
_viewProjection = viewProjection;
_cameraWorldPosition = cameraWorldPosition;
_landscapeTurnsThisFrame = 0;
_currentDcStage = null;
_readyToReplay = false;
_stream.Reset();
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0;
VisitedCells.Clear();
LookInCellTurns.Clear();
_lookInTurns.Clear();
_lookInSlices.Clear();
_lookInPlanes.Clear();
_visibleClipSlotScratch.Clear();
_lookInCyPlane = ctx.CyPlane;
_cellShellsDrawnThisFrame.Clear();
_cellParticleTurnsDrawnThisFrame.Clear();
LookInCells.Clear();
VisitedBuildings.Clear();
VisitedLandscapeCellIds.Clear();
InteriorFloodCells.Clear();
_cellViewRouteIndex = 0;
_landscapeViewRouteIndex = -1;
}
/// <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
{
MarkIfGrown();
MarkAlphaIfGrown();
}
finally
{
_dispatcher.EndWalkPartFrame();
_ctx = null;
_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.
///
/// <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)
{
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.");
}
try
{
if (_stream.Count > 0)
_dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions);
// S3 chunk 3 fix round 1 (F2): cleared at the start of THIS
// replay ("frame start") — a batch never survives across frames.
_pendingTerrainBatch.Clear();
int cursor = 0;
int alphaCursor = 0;
for (int i = 0; i < _events.Count; i++)
{
WalkFrameEvent e = _events[i];
switch (e.Kind)
{
case WalkFrameEventKind.StreamMark:
// 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)
_trace.OnFlush(count, _stream.Stages.GetRange(cursor, count));
_dispatcher.DrawOrderedRange(encoder, cursor, count);
cursor = end;
break;
case WalkFrameEventKind.AlphaSubmitMark:
// 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;
for (; alphaCursor < alphaEnd; alphaCursor++)
{
WbDrawDispatcher.WalkClassifiedBatch batch =
_alphaSubmissions[alphaCursor];
_dispatcher.SubmitWalkAlphaInstance(
in batch,
_cameraWorldPosition,
_viewProjection);
}
break;
case WalkFrameEventKind.Sky:
FlushPendingTerrainBatch();
_leafRenderer.DrawSky();
break;
case WalkFrameEventKind.LandCell:
// 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:
FlushPendingTerrainBatch();
_leafRenderer.DrawCellShell(e.CellId);
break;
case WalkFrameEventKind.PunchFan:
FlushPendingTerrainBatch();
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
break;
case WalkFrameEventKind.AlphaBarrier:
FlushPendingTerrainBatch();
_leafRenderer.AlphaBarrier();
break;
case WalkFrameEventKind.LandscapeFlush:
FlushPendingTerrainBatch();
_leafRenderer.FlushLandscape();
break;
case WalkFrameEventKind.ClearInteriorDepth:
FlushPendingTerrainBatch();
_leafRenderer.ClearInteriorDepth();
break;
case WalkFrameEventKind.ExitSeals:
FlushPendingTerrainBatch();
// S3 §8.2 B2: the driver — not Collect — owns adding
// the leaf's returned submitted-fan count to the
// persistent PortalsDrawnCount, since the real
// enumeration (and therefore the real count) only
// exists once the production leaf actually runs, at
// Replay.
PortalsDrawnCount += _leafRenderer.DrawExitSeals();
break;
case WalkFrameEventKind.StaticParticles:
// S3 chunk 3 fix round 1 (F2): a genuinely empty
// particle turn submits nothing — and, since it
// submits nothing, must NOT flush the pending
// terrain batch either (the batch is order-preserving
// only because a skipped turn changes nothing about
// what reaches the GPU, at all). Retail
// CPhysicsObj::add_particle_shadow_to_cell
// (0x00514a70): an emitter owns one shadow in its OWN
// current cell, drawn at that cell's object turn
// regardless of any owner's registry membership —
// this is a cell lookup, not an owner union.
if (_leafRenderer.HasRenderableEmittersInCell(e.CellId))
{
FlushPendingTerrainBatch();
_leafRenderer.DrawStaticParticles(e.CellId);
}
break;
case WalkFrameEventKind.CellParticles:
if (_leafRenderer.HasRenderableEmittersInCell(e.CellId))
{
FlushPendingTerrainBatch();
_leafRenderer.DrawCellParticles(e.CellId);
}
break;
}
}
// 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
{
_stream.Reset();
_events.Clear();
_markPositions.Clear();
_alphaSubmissions.Clear();
_pendingTerrainBatch.Clear();
_alphaSubmitMark = 0;
_readyToReplay = false;
_ctx = null;
}
}
/// <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 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)
return;
_leafRenderer.DrawLandCellBatch(_pendingTerrainBatch);
_pendingTerrainBatch.Clear();
}
/// <summary>
/// Output-only Campaign FW cathedral trace. The event list described here
/// is the same list <see cref="Replay"/> immediately executes, so this
/// captures the actual clear/seal/punch/shell submission order without
/// inserting a second visibility walk or changing rendering behavior.
/// </summary>
internal void TraceCathedralShellOrder(
ulong frame,
uint viewerCellId,
uint playerCellId,
uint rootCellId,
Vector3 viewerEye)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralShellOrderEnabled
|| !_readyToReplay
|| (frame != 1 && frame % 5 != 0))
{
return;
}
static bool IsCathedralCell(uint cellId) =>
(cellId & 0xFFFF0000u) == 0xF4180000u
&& (cellId & 0xFFFFu) >= 0x0100u;
static bool IsFloatingStairCell(uint cellId) => cellId is
0xF4180104u or 0xF4180106u or 0xF4180107u
or 0xF4180112u or 0xF4180113u or 0xF4180114u;
var order = new System.Text.StringBuilder(1024);
order.Append("[cathedral-shell-order] f=").Append(frame)
.Append(" viewer=0x").Append(viewerCellId.ToString("X8"))
.Append(" player=0x").Append(playerCellId.ToString("X8"))
.Append(" root=0x").Append(rootCellId.ToString("X8"))
.Append(" eye=(").Append(viewerEye.X.ToString("F4"))
.Append(',').Append(viewerEye.Y.ToString("F4"))
.Append(',').Append(viewerEye.Z.ToString("F4")).Append(')')
.Append(" flood=[");
for (int i = 0; i < InteriorFloodCells.Count; i++)
{
if (i != 0)
order.Append(',');
order.Append("0x").Append(InteriorFloodCells[i].ToString("X8"));
}
order.Append("] lookIn=[");
for (int i = 0; i < LookInCellTurns.Count; i++)
{
if (i != 0)
order.Append(',');
order.Append("0x").Append(LookInCellTurns[i].ToString("X8"));
}
order.Append("] replay=");
var clips = new System.Text.StringBuilder(1024);
clips.Append("[cathedral-shell-clips] f=").Append(frame).Append(" slots=");
bool wroteClip = false;
for (int i = 0; i < _events.Count; i++)
{
WalkFrameEvent e = _events[i];
switch (e.Kind)
{
case WalkFrameEventKind.StreamMark:
order.Append('>').Append(i).Append(":M").Append(e.IntArg);
break;
case WalkFrameEventKind.AlphaSubmitMark:
order.Append('>').Append(i).Append(":AM").Append(e.IntArg);
break;
case WalkFrameEventKind.Sky:
order.Append('>').Append(i).Append(":SKY");
break;
case WalkFrameEventKind.LandCell:
// S3 chunk 3 fix round 1 (F6): the trace token now
// carries the LOD side/index too — ":LC<lb>/<side>:<idx>".
order.Append('>').Append(i).Append(":LC")
.Append(e.CellId.ToString("x8"))
.Append('/').Append(e.IntArg >> 8)
.Append(':').Append(e.IntArg & 0xFF);
break;
case WalkFrameEventKind.CellShell when IsCathedralCell(e.CellId):
order.Append('>').Append(i).Append(":S")
.Append((e.CellId & 0xFFFFu).ToString("X4"));
if (IsFloatingStairCell(e.CellId))
{
if (wroteClip)
clips.Append(';');
wroteClip = true;
clips.Append('S')
.Append((e.CellId & 0xFFFFu).ToString("X4"))
.Append("=whole-once");
}
break;
case WalkFrameEventKind.PunchFan:
order.Append('>').Append(i).Append(":P").Append(e.IntArg);
break;
case WalkFrameEventKind.AlphaBarrier:
order.Append('>').Append(i).Append(":AB");
break;
case WalkFrameEventKind.LandscapeFlush:
order.Append('>').Append(i).Append(":LF");
break;
case WalkFrameEventKind.ClearInteriorDepth:
order.Append('>').Append(i).Append(":CLEAR");
break;
case WalkFrameEventKind.ExitSeals:
order.Append('>').Append(i).Append(":SEALS");
break;
}
}
Console.WriteLine(order.ToString());
Console.WriteLine(clips.ToString());
}
// ------------------------------------------------------------------
// IWalkEventSink
// ------------------------------------------------------------------
void IWalkEventSink.Emit(in WalkEvent walkEvent)
{
switch (walkEvent.Kind)
{
case WalkEventKind.DrawInside:
_currentDcStage = WalkDrawStage.CellStatic;
VisitedCells.Add(walkEvent.CellId);
break;
case WalkEventKind.Landscape:
HandleLandscapeTurn(walkEvent.OutsideViewCount);
break;
case WalkEventKind.DrawCells:
foreach (uint id in walkEvent.Cells)
VisitedCells.Add(id);
HandleDrawCellsTurn(walkEvent.Cells);
break;
case WalkEventKind.Building:
// OnBuildingTurn (below) carries the actual side effects —
// this vocabulary-only event needs no driver action.
break;
}
}
/// <summary>S3 chunk 3 (§9.2 B1): records this admitted land cell's
/// terrain turn. <c>sideCellCount</c>/<c>cellIndex</c> travel packed
/// (not expanded) — R3's index-run arithmetic runs at LOD resolution in
/// the production leaf, not at 8×8-bucket resolution like the object-
/// list turn's expansion.</summary>
void IWalkEventSink.OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex)
{
RequireOpenFrame();
if (sideCellCount is not (1 or 2 or 4 or 8))
{
throw new ArgumentOutOfRangeException(
nameof(sideCellCount),
sideCellCount,
"A landscape LOD grid must be 1, 2, 4, or 8 cells per side.");
}
if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount))
throw new ArgumentOutOfRangeException(nameof(cellIndex));
// The one mark rule (this type's own doc comment): flush any
// already-queued stream content ahead of this leaf-renderer event.
MarkIfGrown();
_events.Add(WalkFrameEvent.LandCell(landblockId, sideCellCount, cellIndex));
}
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
=> HandleLandscapeCellTurn(cellId);
void IWalkEventSink.OnLandscapeCellTurn(
uint landblockId,
int sideCellCount,
int cellIndex)
{
if (sideCellCount is not (1 or 2 or 4 or 8))
{
throw new ArgumentOutOfRangeException(
nameof(sideCellCount),
sideCellCount,
"A landscape LOD grid must be 1, 2, 4, or 8 cells per side.");
}
if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount))
throw new ArgumentOutOfRangeException(nameof(cellIndex));
uint blockPrefix = landblockId & 0xFFFF0000u;
if (sideCellCount == 8)
{
HandleLandscapeCellTurn(blockPrefix | checked((uint)(cellIndex + 1)));
return;
}
// CLandBlock's coarse DrawSortCell owns the shadows from every
// original 24 m land cell it covers. Our render journal deliberately
// retains those authoritative 8x8 owner buckets, so turn all covered
// buckets here while preserving the coarse cell's single walk turn.
int span = 8 / sideCellCount;
int coarseX = cellIndex / sideCellCount;
int coarseY = cellIndex % sideCellCount;
int firstX = coarseX * span;
int firstY = coarseY * span;
for (int x = firstX; x < firstX + span; x++)
{
for (int y = firstY; y < firstY + span; y++)
{
HandleLandscapeCellTurn(
blockPrefix | checked((uint)(x * 8 + y + 1)));
}
}
}
private void HandleLandscapeCellTurn(uint cellId)
{
RequireOpenFrame();
if (_landscapeViewRouteIndex < 0)
{
throw new InvalidOperationException(
"A landscape cell turn fired before RetailFrameWalk installed its active "
+ "view set — the walk and draw driver are desynchronized.");
}
VisitedLandscapeCellIds.Add(cellId);
WalkFrameStaticRecords records = _worldData.GetOutdoorStatics(cellId);
WalkFrameStaticRecords dynamics = _worldData.GetOutdoorDynamics(cellId);
_populator.PopulateOutdoorStatics(
_stream, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, _landscapeViewRouteIndex,
alphaSubmissions: _alphaSubmissions);
_populator.PopulateCellDynamics(
_stream, cellId, dynamics.Records, dynamics.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, _landscapeViewRouteIndex,
alphaSubmissions: _alphaSubmissions);
if (_alphaSubmissions.Count != _alphaSubmitMark)
{
MarkIfGrown();
MarkAlphaIfGrown();
}
// FW4 (the #132 positional invariant): this cell's emitters submit
// AT THIS TURN, so nearer buildings' pre-punch barriers drain them
// against still-true depth — see WalkFrameEventKind.StaticParticles.
// Mark first so the cell's own meshes flush ahead of its particle
// submission (retail's per-object DrawObjCell order). Fires
// unconditionally: retail's add_particle_shadow_to_cell draws an
// emitter at its own cell's turn independent of whether that cell
// has any visible static/dynamic owner record (Campaign OVERHAUL S2
// chunk 6 — a suspended/hidden owner's emitter must still show).
MarkIfGrown();
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
}
void IWalkEventSink.OnLandscapeViews(WalkPortalView activeViews)
{
ArgumentNullException.ThrowIfNull(activeViews);
RequireOpenFrame();
_landscapeViewRouteIndex = _cellViewRouteIndex++;
CaptureViews(0, activeViews);
}
void IWalkEventSink.OnBuildingTurn(WalkBuilding building)
{
ArgumentNullException.ThrowIfNull(building);
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).
MarkIfGrown();
MarkAlphaIfGrown();
_events.Add(WalkFrameEvent.AlphaBarrier());
_currentDcStage = WalkDrawStage.LookInStatic;
}
void IWalkEventSink.OnBuildingShellTurn(WalkBuilding building)
{
ArgumentNullException.ThrowIfNull(building);
RequireOpenFrame();
// CPhysicsPart::Draw(parts, 0) @0x0059f331 — retail's plain-mesh
// shell draw, strictly after the portal pass (CPhysicsPart::Draw
// (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);
bool skipSouthBuildingShell =
AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipSouthBuildingShell
&& WalkProductionWorldData.AnchorCellId(building) == 0xF4180112u;
if (!skipSouthBuildingShell)
{
_populator.PopulateCell(
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection,
alphaSubmissions: _alphaSubmissions);
if (_alphaSubmissions.Count != _alphaSubmitMark)
{
MarkIfGrown();
MarkAlphaIfGrown();
}
}
// Campaign OVERHAUL S2 chunk 6: a building's shell fires no particle
// turn of its own any more. Retail's add_particle_shadow_to_cell
// (0x00514a70) gives an emitter exactly one shadow in ITS OWN
// current cell — a building's interior emitters live in their own
// EnvCells and draw at those cells' own CellParticles turns, never
// at their parent building's shell turn.
}
void IWalkEventSink.OnPunchGeometry(
WalkBuilding building, WalkPolygon polygon, int activeViewIndex)
{
ArgumentNullException.ThrowIfNull(building);
ArgumentNullException.ThrowIfNull(polygon);
RequireOpenFrame();
// Output-gated binary discriminator for the cathedral's moving
// transparent seam. Cell 0xF4180112 belongs to a distinct authored
// building which is reached as a look-in while the camera remains in
// 0xF4180107/0108. Suppress only that building's far-Z punch so a
// visual gate can prove whether the apparent "beam" is the aperture
// depth write or the seven-part stair Setup behind it. Never enabled
// in normal production.
if (AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipStairBuildingPunch
&& Array.Exists(
building.Portals,
portal => portal.OtherCellId == 0xF4180112u))
{
return;
}
MarkIfGrown();
Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building);
_events.Add(
WalkFrameEvent.PunchFan(TransformToWorld(polygon, worldTransform), activeViewIndex));
}
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells, int outsideViewCount)
{
ArgumentNullException.ThrowIfNull(cells);
RequireOpenFrame();
// PView::DrawCells @0x005a4840 (S3 §8.1 R3): the landscape flush, the
// device-stamp advance, the gated depth clear, and the exit-portal
// seals ALL sit strictly inside `if (outside_view.view_count > 0)`
// (0x005a4852-0x005a49eb) — outsideViewCount==0 skips straight to
// the flood's own cells below with none of the four having run.
if (outsideViewCount > 0)
{
// D3DPolyRender::FlushAlphaList(0f) @0x005a4872, plus the
// pre-clear dynamics hook RetailPViewRenderer.FlushWalkLandscape
// bundles — the first action inside the gate.
MarkIfGrown();
_events.Add(WalkFrameEvent.LandscapeFlush());
// m_nFrameStamp += 1 @0x005a4886. Its drawn-part AND DrawEnvCell
// dedupe is per render stamp, not per presented frame: content
// admitted during the landscape must remain eligible for the
// interior-cell repaint after the clear. In Collect, every
// landscape candidate has been classified by this point and no
// interior-root candidate has, so re-arming both CPU-side stamp
// mirrors here is the exact boundary. Without the shell re-arm,
// color from a pre-clear building look-in survives while the
// root repaint is incorrectly suppressed, producing
// wall-textured bleed slabs.
_dispatcher.AdvanceWalkPartPassStamp();
_cellShellsDrawnThisFrame.Clear();
_cellParticleTurnsDrawnThisFrame.Clear();
// The gated full depth clear (pc:432731-432732): retail's
// portalsDrawnCount (D3DPolyRender::portalsDrawnCount,
// uint16 @0x008719b4) is read-then-zeroed HERE
// (@0x005a489c-0x005a489e; R4) — forceClear never writes in the
// pseudo-C, so the clear fires iff the counter was nonzero. A
// fresh driver's very first ov>0 frame therefore draws NO clear
// (portalsDrawnCount starts at 0); every later ov>0 frame clears
// because the PREVIOUS frame's own exit seals armed the counter
// at Replay (see PortalsDrawnCount's own doc comment).
int armed = PortalsDrawnCount;
PortalsDrawnCount = 0;
if (armed != 0)
{
MarkIfGrown();
_events.Add(WalkFrameEvent.ClearInteriorDepth());
}
// The exit-portal seals (pc:432785-432786) — this turn's
// submitted fan count re-arms PortalsDrawnCount at Replay (B2),
// read by the NEXT ov>0 frame's decision above.
MarkIfGrown();
_events.Add(WalkFrameEvent.ExitSeals());
}
// FW4 slice 2: retain the ordered flood for the seal draw (the
// DrawExitSeals leaf runs at Replay, when Collect has long filled
// this) — see the property's own doc comment.
InteriorFloodCells.Clear();
for (int i = 0; i < cells.Count; i++)
InteriorFloodCells.Add(cells[i]);
EmitFloodTurns(WalkDrawStage.CellStatic, cells);
}
// ------------------------------------------------------------------
// Turn handlers
// ------------------------------------------------------------------
private void HandleLandscapeTurn(int activeViewCount)
{
RequireOpenFrame();
if (activeViewCount < 1)
{
throw new InvalidOperationException(
$"A Landscape turn fired with {activeViewCount} active views — "
+ "RetailFrameWalk only draws the landscape through an installed view set "
+ "(the outdoor root's full-screen default view, or an interior root's "
+ "surviving exit views, both at least 1). A zero/negative count is a "
+ "walk/driver desync (Campaign FW fail-loud rule).");
}
if (_landscapeTurnsThisFrame != 0)
{
throw new InvalidOperationException(
"A second Landscape turn fired in one frame — RetailFrameWalk.WalkFrame/"
+ "DrawInside's own call graph guarantees at most one Landscape turn per "
+ "frame (outdoor root draws it once; an interior root draws it at most "
+ "once more, through surviving exit views). A second occurrence is a walk/"
+ "driver desync, not something to silently double-draw sky for (Campaign "
+ "FW3.2b-1 fail-loud rule).");
}
MarkIfGrown();
_events.Add(WalkFrameEvent.Sky());
_landscapeTurnsThisFrame++;
// S3 chunk 3 (§9.1 R4/§9.2 B1): the FW4-slice-6 whole-stage terrain
// turn (retail draws terrain blocks ONCE per landscape turn) is
// SUPERSEDED — retail's true per-cell DrawLandCell/DrawSortCell
// interleave now emits one WalkFrameEventKind.LandCell turn per
// admitted land cell (IWalkEventSink.OnLandCellTurn, fired from
// RetailFrameWalk.DrawLandscape's own per-cell loop), not a single
// TerrainSlice(0) here. S3 chunk 3 fix round 1 (F6) corrects the
// direction of this note: drawing ALL terrain BEFORE every building
// let a FARTHER building's far-Z punch survive, because the NEARER
// terrain that retail draws AFTER that punch (and which overwrites
// it) was instead drawn BEFORE it here — nothing later covered the
// punched depth, so a doorway behind a hill punched straight through
// it. The interleave fixes this by ORDER alone: nearer terrain now
// draws AFTER the farther building's punch, exactly like retail. The
// walk's active views still own only the punch fans and dynamics
// apertures; terrain itself is UNCLIPPED for both root kinds.
}
private void HandleDrawCellsTurn(IReadOnlyList<uint> cells)
{
RequireOpenFrame();
if (_currentDcStage is not { } stage)
{
throw new InvalidOperationException(
"A DrawCells turn fired before any DrawInside or Building turn established "
+ "which stage its cells belong to — a walk/driver desync (Campaign FW3.2b-1 "
+ "fail-loud rule): RetailFrameWalk only ever emits DrawCells after DrawInside "
+ "(the interior root's own flood) or after a building's look-in portal pass.");
}
if (stage == WalkDrawStage.CellStatic)
{
// The interior root's OWN flood: this DC event fired at
// breakpoint-ENTRY order (matching the FW0 oracle traces), not
// retail's actual draw point — RECORD only. RetailFrameWalk.
// DrawInside hands the SAME cell list to
// OnInteriorFloodDrawTurn later, at the point retail really
// draws it (after the landscape turn, or immediately when no
// exit views survived) — see that method's doc comment.
return;
}
// 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 —
// record immediately. PView::DrawCells uses two complete reverse
// loops: every EnvCell shell first, then every cell object list.
EmitFloodTurns(stage, cells);
}
private void EmitFloodTurns(WalkDrawStage stage, IReadOnlyList<uint> cells)
{
_floodViewRouteScratch.Clear();
for (int i = 0; i < cells.Count; i++)
_floodViewRouteScratch.Add(-1);
// PView::DrawCells @0x005A4840, loop 2 (005A4A00005A4ADE):
// cell_draw_list[count - 1] down to zero. The loop calls setup_view +
// DrawEnvCell for each live view, but DrawEnvCell @0x0059F170 begins
// with GetDrawnThisFrame/SetDrawnThisFrame. Its built-mesh path then
// calls D3DPolyRender::DrawMesh directly: the EnvCell shell is drawn
// WHOLE on the first call and every later view/cross-flood call is a
// no-op for this render stamp. Portal-view clipping remains active for
// the following object-list loop; it is not a shell geometry clip.
for (int i = cells.Count - 1; i >= 0; i--)
{
int viewRouteIndex = CaptureCellViewRoute(cells[i]);
_floodViewRouteScratch[i] = viewRouteIndex;
if (_cellShellsDrawnThisFrame.Add(cells[i]))
{
bool skipSouthLookInShell =
stage == WalkDrawStage.LookInStatic
&& ((AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipSouthLookInCellShells
&& cells[i] is 0xF4180112u or 0xF4180113u or 0xF4180114u)
|| AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipLookInShellCellId == cells[i]);
if (!skipSouthLookInShell)
{
MarkIfGrown();
_events.Add(WalkFrameEvent.CellShell(cells[i]));
}
}
}
// Loop 3 (005A4ADE005A4B2D): restart at count - 1 and draw each
// complete object cell. Keep a stream mark per cell so the packed
// static records and its dynamic/particle tail stay at that exact
// retail turn.
for (int i = cells.Count - 1; i >= 0; i--)
EmitCellContentsTurn(stage, cells[i], _floodViewRouteScratch[i]);
}
private int CaptureCellViewRoute(uint cellId)
{
int viewRouteIndex = _cellViewRouteIndex++;
CaptureCellViews(cellId);
return viewRouteIndex;
}
private void EmitCellContentsTurn(
WalkDrawStage stage,
uint cellId,
int viewRouteIndex)
{
// The shell loop captured the exact live portal_view for this cell.
// Static and dynamic object lists reuse that same route: retail's
// DrawEnvCell and DrawObjCell consume the same PortalList.
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
_populator.PopulateCell(
_stream, stage, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection,
this, viewRouteIndex, _alphaSubmissions);
MarkIfGrown();
MarkAlphaIfGrown();
if (stage == WalkDrawStage.LookInStatic)
{
LookInCellTurns.Add(cellId);
LookInCells.Add(cellId);
}
WalkFrameStaticRecords dynamics = _worldData.GetCellDynamics(cellId);
_populator.PopulateCellDynamics(
_stream,
cellId,
dynamics.Records,
dynamics.TupleLandblockId,
_cameraWorldPosition,
_viewProjection,
this,
viewRouteIndex,
alphaSubmissions: _alphaSubmissions);
MarkIfGrown();
MarkAlphaIfGrown();
// Fires for every cell turn regardless of whether that cell has any
// visible static/dynamic owner record (Campaign OVERHAUL S2 chunk 6 —
// a suspended/hidden owner's emitter must still show), but ONCE per
// render stamp: retail's particle parts sit in the same shadow_part_list
// as every other part, so CPhysicsPart::Draw's frame stamp suppresses a
// second submission when one cell gets two object-list turns in one
// frame (a chamber reached through two portals). The same frame-scoped
// set that dedupes the cell SHELL dedupes the particle turn (chunk 6
// review F1).
if (_cellParticleTurnsDrawnThisFrame.Add(cellId))
_events.Add(WalkFrameEvent.CellParticles(cellId));
}
private void CaptureCellViews(uint cellId)
{
IWalkBuildingFrameContext ctx = RequireOpenFrame();
WalkCell? cell = ctx.GetVisible(cellId);
if (cell is null || cell.NumView <= 0)
{
_lookInTurns.Add(new WalkLookInTurn(
cellId, _lookInSlices.Count, 0));
return;
}
CaptureViews(cellId, cell.TopView);
}
private void CaptureViews(uint cellId, WalkPortalView portalView)
{
int sliceStart = _lookInSlices.Count;
for (int sliceIndex = 0; sliceIndex < portalView.ViewCount; sliceIndex++)
{
WalkViewPoly poly = portalView.View.Polys[sliceIndex];
int planeStart = _lookInPlanes.Count;
for (int edge = 0; edge < poly.VertexCount; edge++)
{
_lookInPlanes.Add(
portalView.View.Vertices[poly.VertexIndex + edge].Plane);
}
uint clipSlot = AppendClipSlot(portalView, poly);
_lookInSlices.Add(new WalkLookInSlice(
planeStart, poly.VertexCount, clipSlot));
}
_lookInTurns.Add(new WalkLookInTurn(
cellId, sliceStart, _lookInSlices.Count - sliceStart));
}
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
return false;
WalkLookInTurn turn = _lookInTurns[routeIndex];
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
{
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
if (WalkVisibilityMath.ViewconeCheck(
center,
radius,
_lookInCyPlane,
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
slice.PlaneStart,
slice.PlaneCount)) != WalkBoundingType.Outside)
{
return true;
}
}
return false;
}
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
int routeIndex,
in Vector3 center,
float radius,
bool testSphere)
{
_visibleClipSlotScratch.Clear();
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
return _visibleClipSlotScratch;
WalkLookInTurn turn = _lookInTurns[routeIndex];
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
{
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
if (!testSphere
|| WalkVisibilityMath.ViewconeCheck(
center,
radius,
_lookInCyPlane,
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
slice.PlaneStart,
slice.PlaneCount)) != WalkBoundingType.Outside)
{
_visibleClipSlotScratch.Add(slice.ClipSlot);
}
}
return _visibleClipSlotScratch;
}
public string DescribeLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
return "route-missing";
WalkLookInTurn turn = _lookInTurns[routeIndex];
var description = new System.Text.StringBuilder(192);
float cyDistance = Vector3.Dot(_lookInCyPlane.Normal, center)
+ _lookInCyPlane.D;
description.Append("turnCell=0x")
.Append(turn.CellId.ToString("X8"))
.Append(" slices=").Append(turn.SliceCount)
.Append(" cy=").Append(cyDistance.ToString("F5"))
.Append(" cyMargin=").Append((cyDistance + radius).ToString("F5"));
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
{
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
description.Append(" slice[").Append(sliceOffset).Append("]=");
for (int planeOffset = 0; planeOffset < slice.PlaneCount; planeOffset++)
{
if (planeOffset != 0)
description.Append(',');
WalkPlane plane = _lookInPlanes[slice.PlaneStart + planeOffset];
float distance = Vector3.Dot(plane.Normal, center) + plane.D;
description.Append(distance.ToString("F5"));
}
}
return description.ToString();
}
/// <summary>Converts one retail pixel-space portal_view polygon into the
/// clip-space half-planes consumed by mesh_modern.vert. copy_view already
/// deduplicates and removes collinear vertices, so 3..8 points map
/// directly. The rare >8-edge case uses its convex AABB as a conservative
/// four-plane gate rather than falling back to slot 0 (which would erase
/// the aperture entirely).</summary>
private uint AppendClipSlot(WalkPortalView portalView, WalkViewPoly poly)
{
if (_clipFrame is null)
return 0;
IRetailFrameWalkContext ctx = (IRetailFrameWalkContext)RequireOpenFrame();
int count = poly.VertexCount;
if (count < 3)
return 0;
Span<Vector2> ndc = stackalloc Vector2[Math.Min(count, WalkCopyView.MaxVertices)];
float minX = float.MaxValue, minY = float.MaxValue;
float maxX = float.MinValue, maxY = float.MinValue;
for (int i = 0; i < ndc.Length; i++)
{
Vector2 px = portalView.View.Vertices[poly.VertexIndex + i].Point;
Vector2 point = new(
px.X / ctx.ViewportWidth * 2f - 1f,
1f - px.Y / ctx.ViewportHeight * 2f);
ndc[i] = point;
minX = MathF.Min(minX, point.X);
minY = MathF.Min(minY, point.Y);
maxX = MathF.Max(maxX, point.X);
maxY = MathF.Max(maxY, point.Y);
}
if (ndc.Length > ClipFrame.MaxPlanes)
{
Span<Vector4> aabbPlanes = stackalloc Vector4[4]
{
new(1f, 0f, 0f, -minX),
new(-1f, 0f, 0f, maxX),
new(0f, 1f, 0f, -minY),
new(0f, -1f, 0f, maxY),
};
return checked((uint)_clipFrame.AppendSlot(aabbPlanes));
}
float area2 = 0f;
for (int i = 0; i < ndc.Length; i++)
area2 += ndc[i].X * ndc[(i + 1) % ndc.Length].Y
- ndc[(i + 1) % ndc.Length].X * ndc[i].Y;
bool ccw = area2 >= 0f;
Span<Vector4> planes = stackalloc Vector4[ClipFrame.MaxPlanes];
for (int i = 0; i < ndc.Length; i++)
{
int current = ccw ? i : ndc.Length - 1 - i;
int next = ccw
? (i + 1) % ndc.Length
: (ndc.Length - 2 - i + ndc.Length) % ndc.Length;
Vector2 p = ndc[current];
Vector2 q = ndc[next];
Vector2 dir = q - p;
Vector2 normal = Vector2.Normalize(new Vector2(-dir.Y, dir.X));
planes[i] = new Vector4(normal.X, normal.Y, 0f, -Vector2.Dot(normal, p));
}
return checked((uint)_clipFrame.AppendSlot(planes[..ndc.Length]));
}
/// <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()
{
int count = _stream.Count;
int last = _markPositions.Count > 0 ? _markPositions[^1] : 0;
if (count == last)
return;
_markPositions.Add(count);
_events.Add(WalkFrameEvent.Mark(count));
}
/// <summary>Records translucent walk batches at the same temporal point
/// as their cell turn. Replay submits them only when it reaches this
/// marker, so an earlier building's full alpha barrier cannot see content
/// that the far-to-near walk has not reached yet.</summary>
private void MarkAlphaIfGrown()
{
int count = _alphaSubmissions.Count;
if (count == _alphaSubmitMark)
return;
_alphaSubmitMark = count;
_events.Add(WalkFrameEvent.AlphaSubmitMark(count));
}
private IWalkBuildingFrameContext RequireOpenFrame() =>
_ctx ?? throw new InvalidOperationException(
"WalkFrameDriver received a walk turn outside BeginFrame/EndFrame — call "
+ "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
/// normal uses <see cref="Vector3.TransformNormal"/> (correct for the
/// rigid, shear-free placements <see cref="WalkBuildingFactory"/> and
/// <see cref="WalkCellFactory"/> build) and <c>D</c> is rederived from
/// the transformed normal and the first transformed vertex — the SAME
/// construction <see cref="WalkCellFactory"/>/<see cref="WalkBuildingFactory"/>
/// already use for their own polygons (<c>Plane = new WalkPlane(normal,
/// -Vector3.Dot(normal, vertices[0]))</c>).</summary>
private static WalkPolygon TransformToWorld(WalkPolygon local, Matrix4x4 worldTransform)
{
var vertices = new Vector3[local.Vertices.Length];
for (int i = 0; i < vertices.Length; i++)
vertices[i] = Vector3.Transform(local.Vertices[i], worldTransform);
Vector3 normal = local.Vertices.Length > 0
? Vector3.Normalize(Vector3.TransformNormal(local.Plane.Normal, worldTransform))
: Vector3.Zero;
float d = vertices.Length > 0 ? -Vector3.Dot(normal, vertices[0]) : 0f;
return new WalkPolygon { Vertices = vertices, Plane = new WalkPlane(normal, d) };
}
}