fix(render) Campaign FW3.2b-1: interior draw order - landscape before cells

The oracle trace order DI|DC|LS is breakpoint-ENTRY order; retail's
actual DRAW order inside PView::DrawCells @0x005a4840 for an interior
root is LScape::draw FIRST (pc:432719, only when exit views survive),
then the depth clear (pc:432731-432732), the exit-portal seals
(pc:432785-432786), THEN the flood's own cells far-to-near. The
driver drew flood cells before the landscape - inverted.

RetailFrameWalk.DrawInside gains the additive
OnInteriorFloodDrawTurn(cells) hook firing after the conditional
landscape turn; the DC EVENT stays at its original point (conformance
untouched - 40/1 InstalledDat green). WalkFrameDriver records the
interior flood at the DC turn and draws it at the new turn:
flush -> ClearInteriorDepth -> flush -> DrawExitSeals -> per-cell
shell-then-contents. Building look-in floods still draw immediately at
their building turn (retail's reentrant DrawCells with no clear/seal).
Two new leaf members map to IWorldPassScope.ClearInteriorDepth and the
seal-fan machinery at FW3.2b-2. Reconciliation note recorded: the
driver clears unconditionally for interior roots while production
stages the clear on OutsideViewSlices>0 - observably equivalent at
ov=0, awaiting a firmer decomp read of the clear's gate.

Suites: full Release build 0 warnings; Walk lane 201/1 skip;
hermetic 6,753/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 14:34:45 +02:00
parent 03f63686cc
commit 035d7b0148
4 changed files with 247 additions and 20 deletions

View file

@ -93,7 +93,18 @@ public sealed class RetailFrameWalk
}
/// <summary><c>PView::DrawInside</c> + the event half of
/// <c>DrawCells</c>. The geometry/object passes emit no walk events.</summary>
/// <c>DrawCells</c>. The geometry/object passes emit no walk events.
/// <c>PView::DrawCells</c> @0x005a4840's actual DRAW order for the root
/// flood is NOT the order its events fire in: the
/// <see cref="WalkEventKind.DrawCells"/> <see cref="IWalkEventSink.Emit"/>
/// call below fires at breakpoint-ENTRY order (matching the FW0 oracle
/// traces, whose breakpoint sat at <c>DrawCells</c> entry — before
/// retail has drawn anything), but retail itself draws
/// <c>LScape::draw</c> FIRST (pc:432719, only when exit views survived),
/// then a gated full depth clear (pc:432731-432732), then the exit-
/// portal seals (pc:432785-432786), and ONLY THEN the flood's own cells
/// far-to-near. <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>
/// fires at that later point (see its own doc comment).</summary>
public void DrawInside(
WalkCell cell, WalkLandscape landscape,
IRetailFrameWalkContext ctx, IWalkEventSink sink)
@ -106,10 +117,16 @@ public sealed class RetailFrameWalk
ctx.ViewportWidth, ctx.ViewportHeight);
_interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
EmitDrawCells(_interiorPView, sink);
uint[] floodCells = EmitDrawCells(_interiorPView, sink);
if (_interiorPView.OutsideView.ViewCount > 0)
DrawLandscape(landscape, _interiorPView.OutsideView, ctx, sink);
// Additive (Campaign FW3.2b-1): see this method's own doc comment —
// the flood's actual cell-drawing turn, unconditional of whether a
// landscape turn just ran (ov==0 skips straight here from the DC
// event above).
sink.OnInteriorFloodDrawTurn(floodCells);
RemoveViews(cell.StabList, ctx);
cell.PopView();
}
@ -213,12 +230,16 @@ public sealed class RetailFrameWalk
sink.OnBuildingShellTurn(building);
}
private void EmitDrawCells(WalkPView pview, IWalkEventSink sink)
/// <summary>Returns the extracted cell-id array so <see cref="DrawInside"/>
/// can hand the SAME flood list to <c>OnInteriorFloodDrawTurn</c> later,
/// without re-walking <c>pview.CellDrawList</c> a second time.</summary>
private uint[] EmitDrawCells(WalkPView pview, IWalkEventSink sink)
{
uint[] cells = new uint[pview.CellDrawList.Count];
for (int i = 0; i < cells.Length; i++)
cells[i] = pview.CellDrawList[i].CellId;
sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
return cells;
}
private void AddViews(uint[] stabList, IRetailFrameWalkContext ctx)

View file

@ -53,7 +53,7 @@ public readonly record struct WalkEvent(
/// production sink will additionally receive the ordered draw stream.
///
/// <para>Campaign FW3.2b-1 additive seam: <see cref="RetailFrameWalk"/> also
/// calls the four richer, default-no-op members below at turns the
/// calls the five richer, default-no-op members below at turns the
/// vocabulary-only <see cref="WalkEvent"/> stream cannot express (a visited
/// landscape cell with no building emits no <see cref="WalkEvent"/> at all;
/// <see cref="WalkEventKind.Building"/> carries only a cell id, not the
@ -125,4 +125,29 @@ public interface IWalkEventSink
/// no-op.
/// </summary>
void OnPunchGeometry(WalkBuilding building, WalkPolygon polygon) { }
/// <summary>
/// Fires once per interior root, at the point <c>PView::DrawCells</c>
/// @0x005a4840 actually DRAWS the root flood's own cells — NOT where the
/// <see cref="WalkEventKind.DrawCells"/> <see cref="Emit"/> call for the
/// SAME flood fires (that one sits at breakpoint-ENTRY order, matching
/// the FW0 oracle traces; it only RECORDS the flood list). Retail's own
/// order inside <c>DrawCells</c> is: <c>LScape::draw</c> FIRST
/// (pc:432719, only when exit views survived — see
/// <see cref="RetailFrameWalk.DrawLandscape"/> and
/// <see cref="WalkEventKind.Landscape"/>), then a full depth clear
/// (pc:432731-432732), then the exit-portal seals (pc:432785-432786) —
/// BOTH unconditional for an interior root's
/// own flood, whether or not a landscape turn just ran — and ONLY THEN
/// the flood's cells far-to-near (shell then contents per cell, same
/// discipline as <see cref="OnLandscapeCellTurn"/>'s per-cell contents
/// and a building's look-in). This hook fires at that later point, so
/// this is where a driver should actually draw <paramref name="cells"/>.
/// Building look-in floods are UNAFFECTED — retail calls
/// <c>DrawCells</c> re-entrantly there with <c>ov==0</c> and no
/// landscape/clear/seal step, so their <see cref="WalkEventKind.DrawCells"/>
/// <see cref="Emit"/> call still fires at the actual draw point (a
/// driver may keep drawing those immediately, as before). Default no-op.
/// </summary>
void OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells) { }
}

View file

@ -63,7 +63,8 @@ internal interface IWalkFrameWorldData
/// 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) or that aren't a draw at all (the
/// 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
@ -109,6 +110,35 @@ internal interface IWalkFrameLeafRenderer
/// this same shell-then-contents order).</summary>
void DrawCellShell(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>, staged there on <c>OutsideViewSlices.Length
/// &gt; 0</c> — an ACKNOWLEDGED approximation of retail's true
/// <c>portalsDrawnCount</c> gate per that file's own comment). This walk
/// driver instead fires unconditionally for every interior root (per the
/// 2026-08-30 decomp correction: the coordinator's directive supersedes
/// the packed path's staged gate — reconcile the two if a firmer
/// <c>portalsDrawnCount</c> reading ever lands). 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).</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"/>, 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. 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>
void DrawExitSeals();
/// <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
@ -172,28 +202,38 @@ internal interface IWalkFrameDriverTrace
///
/// <para><b>The one flush rule that reproduces the whole frame script:</b>
/// before EVERY leaf-renderer call (<see cref="IWalkFrameLeafRenderer.DrawSky"/>,
/// <c>DrawTerrainSlice</c>, <c>DrawCellShell</c>, <c>DrawPunchFan</c>) and
/// before every <see cref="IWalkFrameLeafRenderer.AlphaBarrier"/> call, the
/// driver flushes the accumulated opaque stream (a no-op when the stream is
/// empty — "empty segments submit nothing"); a building's own shell content
/// is APPENDED (not flushed) the moment <see cref="IWalkEventSink.OnBuildingShellTurn"/>
/// <c>DrawTerrainSlice</c>, <c>DrawCellShell</c>, <c>ClearInteriorDepth</c>,
/// <c>DrawExitSeals</c>, <c>DrawPunchFan</c>) and before every
/// <see cref="IWalkFrameLeafRenderer.AlphaBarrier"/> call, the driver
/// flushes the accumulated opaque stream (a no-op when the stream is empty —
/// "empty segments submit nothing"); a building's own shell content is
/// APPENDED (not flushed) the moment <see cref="IWalkEventSink.OnBuildingShellTurn"/>
/// fires, so it flushes only at whatever non-stream action comes next (the
/// next building's alpha barrier, or end of frame). This single rule,
/// combined with "shell before contents" per cell and retail's own building
/// combined with "shell before contents" per cell, retail's own building
/// order (alpha barrier → portal pass → shell — see
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), is what
/// produces every ordering constraint the plan's frame script names: [cell1
/// shell] [cell1 contents flush] [cell2 shell] …, [alpha barrier] [punch fan(s)
/// + look-in flood(s), each following the SAME shell-then-contents per-cell
/// discipline] [building shell content flush], and the final end-of-frame
/// flush. No special-casing per turn kind is needed beyond that.</para>
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
/// own interior-root DRAW order (landscape → clear → seals → the flood's own
/// cells — 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: [cell1 shell]
/// [cell1 contents flush] [cell2 shell] …, [alpha barrier] [punch fan(s) +
/// look-in flood(s), each following the SAME shell-then-contents per-cell
/// discipline] [building shell content flush], [landscape (if exit views
/// survived)] [interior depth clear] [exit-portal seals] [the interior
/// root's own flood cells], and the final end-of-frame flush. 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
/// (<c>DrawEnvCell</c> @0x005a4abe before <c>DrawObjCellForDummies</c>
/// @0x005a4b0d per flooded cell), <c>RenderDeviceD3D::DrawBuilding</c>
/// @0x005a4b0d per flooded cell; <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>
@ -395,6 +435,26 @@ internal sealed class WalkFrameDriver : IWalkEventSink
_leafRenderer.DrawPunchFan(TransformToWorld(polygon, worldTransform));
}
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells)
{
ArgumentNullException.ThrowIfNull(cells);
RequireOpenFrame();
// PView::DrawCells @0x005a4840: the gated full depth clear
// (pc:432731-432732) then the exit-portal seals (pc:432785-432786) —
// both unconditional for an interior root's own flood, whether or
// not a landscape turn just ran (see this driver's type doc
// comment).
FlushIfNonEmpty();
_leafRenderer.ClearInteriorDepth();
FlushIfNonEmpty();
_leafRenderer.DrawExitSeals();
for (int i = 0; i < cells.Count; i++)
EmitCellTurn(WalkDrawStage.CellStatic, cells[i]);
}
// ------------------------------------------------------------------
// Turn handlers
// ------------------------------------------------------------------
@ -432,6 +492,22 @@ internal sealed class WalkFrameDriver : IWalkEventSink
+ "(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 —
// draw immediately, unchanged from before this correction.
for (int i = 0; i < cells.Count; i++)
EmitCellTurn(stage, cells[i]);
}