feat(render): S3 chunk 2 — gate the interior turn on the real outside-view count

RetailFrameWalk.DrawInside now passes _interiorPView.OutsideView.ViewCount
into IWalkEventSink.OnInteriorFloodDrawTurn. WalkFrameDriver's own
implementation reproduces PView::DrawCells @0x005a4840's exact gating
(0x005a4852-0x005a49eb, all inside `if (outside_view.view_count > 0)`):
the landscape flush (retail FlushAlphaList(0f) @0x005a4872 plus the
pre-clear dynamics hook), the device-stamp advance @0x005a4886, a GATED
depth clear (pc:432731-432732), and the exit-portal seals
(pc:432785-432786) — all four skipped entirely when outsideViewCount == 0.

The depth clear is gated on a new driver field, PortalsDrawnCount, which
models retail's D3DPolyRender::portalsDrawnCount (uint16 @0x008719b4):
read-then-zeroed at the interior root's own flood turn
(@0x005a489c-0x005a489e), and re-armed at Replay by the count
IWalkFrameLeafRenderer.DrawExitSeals now returns (the SAME portal
enumeration RetailPViewPassExecutor.DrawPortalDepthWrite already performs
— OtherCellId==0xFFFF, >=3 vertices). The field persists across frames
(never cleared by BeginFrame/AbortFrame/EndFrame/Replay), reproducing
retail's documented quirk: a fresh driver's first ov>0 frame never clears;
every later ov>0 frame clears because the previous frame's own seals armed
the counter.

_skyDrawnThisFrame — the proxy for outside_view.view_count != 0 that used
to gate the stamp re-arm — is deleted; its "second Landscape turn in one
frame" fail-loud guard moves to a frame-scoped counter
(_landscapeTurnsThisFrame). RetailPViewRenderer.ClearWalkInteriorDepth
splits into FlushWalkLandscape (pre-clear dynamics + FlushLandscapeAlpha)
and ClearWalkInteriorDepth (the Z clear only), both wired through the new
IWalkFrameLeafRenderer.FlushLandscape leaf and the WalkLeaf production
adapter.

Tests: flipped the ov==0 pin to expect no landscape-flush/clear/seals at
all (T1); added the two-frame first-frame-no-clear / armed-clear pin plus
a no-exit-portal-never-clears pin (T2); added a look-in-neither-arms-
nor-consumes-the-counter pin (T3); added RetailFrameWalk's two-PView
draw_landscape wiring pin and a WalkPView.ConstructView reset pin (T4);
updated every direct OnInteriorFloodDrawTurn caller to pass the
outsideViewCount it models (T5). The four per-category leaf-contract pins
(whole-once shell, Boolean sphere admission, portal-polygon-only clip,
local-player repeated submission) already existed and needed no additions
(B4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 07:43:43 +02:00
parent 51d5323208
commit be9b4c1f29
9 changed files with 607 additions and 153 deletions

View file

@ -191,9 +191,13 @@ internal sealed partial class RetailPViewPassExecutor
/// with <c>forceFarZ</c>, clipped by the pinned view's slice planes
/// (retail <c>building_view</c> @0x0059f3bf); FW3.3 draws fans at the dat
/// aperture verbatim (the ShellDrawLiftZ retirement).</item>
/// <item><see cref="ClearInteriorDepth"/>/<see cref="DrawExitSeals"/> →
/// caller-supplied actions (the renderer owns the pass scope and the
/// root-flood seal iteration; the adapter only provides the turns).</item>
/// <item><see cref="FlushLandscape"/>/<see cref="ClearInteriorDepth"/>/
/// <see cref="DrawExitSeals"/> → caller-supplied delegates (the renderer
/// owns the pass scope, the pre-clear dynamics phase, and the root-flood
/// seal iteration; the adapter only provides the turns).
/// <see cref="DrawExitSeals"/> returns the submitted seal-polygon count so
/// the driver can re-arm its persistent <c>PortalsDrawnCount</c> (S3 §8.2
/// B2).</item>
/// <item><see cref="AlphaBarrier"/> → <c>FlushLandscapeAlpha</c>, retail's
/// flush-all <c>FlushAlphaList(0f)</c> @0x0059f30b.</item>
/// </list>
@ -203,8 +207,9 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
private RetailPViewPassExecutor _passes = null!;
private RetailPViewFrameInput _frame = null!;
private ClipFrameAssembly _clipAssembly = null!;
private Action _flushLandscape = null!;
private Action _clearInteriorDepth = null!;
private Action _drawExitSeals = null!;
private Func<int> _drawExitSeals = null!;
private readonly HashSet<uint> _singleCellScratch = new();
private readonly List<uint> _singleCellListScratch = new();
@ -212,9 +217,10 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
RetailPViewPassExecutor passes,
RetailPViewFrameInput frame,
ClipFrameAssembly clipAssembly,
Action flushLandscape,
Action clearInteriorDepth,
Action drawExitSeals)
=> Reset(passes, frame, clipAssembly, clearInteriorDepth, drawExitSeals);
Func<int> drawExitSeals)
=> Reset(passes, frame, clipAssembly, flushLandscape, clearInteriorDepth, drawExitSeals);
/// <summary>
/// FW6 allocation closeout: bind the retained leaf and its retained
@ -225,13 +231,16 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
RetailPViewPassExecutor passes,
RetailPViewFrameInput frame,
ClipFrameAssembly clipAssembly,
Action flushLandscape,
Action clearInteriorDepth,
Action drawExitSeals)
Func<int> drawExitSeals)
{
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
_frame = frame ?? throw new ArgumentNullException(nameof(frame));
_clipAssembly = clipAssembly
?? throw new ArgumentNullException(nameof(clipAssembly));
_flushLandscape = flushLandscape
?? throw new ArgumentNullException(nameof(flushLandscape));
_clearInteriorDepth = clearInteriorDepth
?? throw new ArgumentNullException(nameof(clearInteriorDepth));
_drawExitSeals = drawExitSeals
@ -272,6 +281,8 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
}
}
public void FlushLandscape() => _flushLandscape();
public void ClearInteriorDepth() => _clearInteriorDepth();
public void DrawStaticParticles(uint cellId) =>
@ -280,7 +291,7 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
public void DrawCellParticles(uint cellId) =>
_passes.DrawCellParticles(_frame, cellId);
public void DrawExitSeals() => _drawExitSeals();
public int DrawExitSeals() => _drawExitSeals();
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
_passes.DrawWalkPunchFan(_frame, _clipAssembly, worldPolygon, activeViewIndex);

View file

@ -300,7 +300,11 @@ public RetailPViewPassExecutor(
_surface.ClearInteriorDepth();
}
public void DrawExitPortalMask(
/// <summary>Returns the number of exit-seal fan polygons actually
/// submitted (S3 §8.2 B2) — the SAME enumeration that draws them, so the
/// caller's persistent seal counter always matches what reached the GPU
/// this turn.</summary>
public int DrawExitPortalMask(
RetailPViewFrameInput frame,
uint cellId,
ReadOnlySpan<Vector4> clipPlanes) =>
@ -376,29 +380,34 @@ public RetailPViewPassExecutor(
frame.PlayerViewPosition,
frame.CameraCellResolution);
private void DrawPortalDepthWrite(
/// <summary>Retail <c>D3DPolyRender::DrawPortalPolyInternal</c>
/// @0x0059BC90. Main interior roots stamp true depth (seal); outdoor and
/// look-in apertures stamp far depth (punch). The renderer owns that
/// choice. Returns the number of portals actually submitted — S3 §8.2 B2:
/// each portal with <c>OtherCellId == 0xFFFF</c> and &gt;=3 vertices —
/// the same enumeration <c>WalkFrameDriver.OnInteriorFloodDrawTurn</c>
/// counts through <c>DrawExitSeals</c>'s return value.</summary>
private int DrawPortalDepthWrite(
uint cellId,
ReadOnlySpan<Vector4> clipPlanes,
RetailPViewFrameInput frame,
bool forceFarZ,
int? onlyPortalIndex = null)
{
// Retail D3DPolyRender::DrawPortalPolyInternal @ 0x0059BC90.
// Main interior roots stamp true depth (seal); outdoor and look-in
// apertures stamp far depth (punch). The renderer owns that choice.
if (_portalDepthMask is null)
return;
return 0;
if (!forceFarZ
&& AcDream.Core.Rendering.RenderingDiagnostics
.ProbeCathedralSkipFloatingStairSeals
&& cellId is 0xF4180107u or 0xF4180112u)
{
return;
return 0;
}
LoadedCell? cell = frame.Cells.Find(cellId);
if (cell is null)
return;
return 0;
int submitted = 0;
Span<Vector3> world = stackalloc Vector3[32];
for (int index = 0; index < cell.Portals.Count; index++)
{
@ -435,7 +444,9 @@ public RetailPViewPassExecutor(
frame.ViewProjection,
clipPlanes,
forceFarZ);
submitted++;
}
return submitted;
}
private bool BeginDoorwayScissor(Vector4 ndcAabb) =>

View file

@ -46,8 +46,9 @@ internal sealed class RetailPViewRenderer
private Walk.WalkFrameDriver? _walkFrameDriverScratch;
private Walk.WalkProductionFrameContext? _walkFrameContextScratch;
private WalkProductionLeafRenderer? _walkLeafRendererScratch;
private readonly Action _walkFlushLandscapeAction;
private readonly Action _walkClearInteriorDepthAction;
private readonly Action _walkDrawExitSealsAction;
private readonly Func<int> _walkDrawExitSealsFunc;
private RetailPViewPassExecutor? _activeWalkPasses;
private RetailPViewFrameInput? _activeWalkFrame;
private ClipFrameAssembly? _activeWalkClipAssembly;
@ -84,8 +85,9 @@ internal sealed class RetailPViewRenderer
_walkWorldData = new Walk.WalkProductionWorldData(
_walkBuildings,
shadows ?? throw new ArgumentNullException(nameof(shadows)));
_walkFlushLandscapeAction = FlushWalkLandscape;
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
_walkDrawExitSealsAction = DrawWalkExitSeals;
_walkDrawExitSealsFunc = DrawWalkExitSeals;
}
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
@ -219,8 +221,9 @@ internal sealed class RetailPViewRenderer
walkExecutor,
ctx,
clipAssembly,
_walkFlushLandscapeAction,
_walkClearInteriorDepthAction,
_walkDrawExitSealsAction);
_walkDrawExitSealsFunc);
}
else
{
@ -228,8 +231,9 @@ internal sealed class RetailPViewRenderer
walkExecutor,
ctx,
clipAssembly,
_walkFlushLandscapeAction,
_walkClearInteriorDepthAction,
_walkDrawExitSealsAction);
_walkDrawExitSealsFunc);
}
if (_walkFrameDriverScratch is null)
@ -496,7 +500,15 @@ internal sealed class RetailPViewRenderer
private readonly Walk.RetailFrameWalk _frameWalk = new();
private void ClearWalkInteriorDepth()
/// <summary>S3 chunk 2 (§8.2 B3): the flush half of the former combined
/// <c>ClearWalkInteriorDepth</c> — retail's <c>D3DPolyRender::FlushAlphaList(0f)</c>
/// @0x005a4872 plus the pre-clear dynamics phase (FW3 visual-gate fix:
/// retail draws outside objects inside <c>LScape::draw</c>, before the
/// clear+seals). Called by the driver's <c>LandscapeFlush</c> leaf,
/// unconditionally whenever an interior root's own flood has a
/// surviving exit view (<c>ov&gt;0</c>) — see
/// <see cref="Walk.IWalkFrameLeafRenderer.FlushLandscape"/>.</summary>
private void FlushWalkLandscape()
{
RetailPViewPassExecutor passes = _activeWalkPasses
?? throw new InvalidOperationException(
@ -507,10 +519,28 @@ internal sealed class RetailPViewRenderer
// LScape::draw, before the clear+seals.
_walkPreClearDynamics?.Invoke();
passes.FlushLandscapeAlpha();
}
/// <summary>S3 chunk 2 (§8.2 B3): the Z-clear half of the former combined
/// <c>ClearWalkInteriorDepth</c> — <c>PView::DrawCells</c>'s gated full
/// depth clear (pc:432731-432732). The driver only calls this leaf when
/// its persistent <c>PortalsDrawnCount</c> was nonzero at the
/// read-then-zero decision (S3 §8.1 R4) — see
/// <see cref="Walk.IWalkFrameLeafRenderer.ClearInteriorDepth"/>.</summary>
private void ClearWalkInteriorDepth()
{
RetailPViewPassExecutor passes = _activeWalkPasses
?? throw new InvalidOperationException(
"The retained walk leaf has no active pass binding.");
passes.ClearInteriorDepth();
}
private void DrawWalkExitSeals()
/// <summary>Returns the total exit-seal fan count submitted this turn
/// (S3 §8.2 B2), so the driver can re-arm its persistent
/// <c>PortalsDrawnCount</c> for the NEXT <c>ov&gt;0</c> interior-root
/// flood's clear decision.</summary>
private int DrawWalkExitSeals()
{
RetailPViewFrameInput frame = _activeWalkFrame
?? throw new InvalidOperationException(
@ -522,7 +552,7 @@ internal sealed class RetailPViewRenderer
?? throw new InvalidOperationException(
"The retained walk leaf has no active driver binding.");
DrawWalkExitPortalMasks(frame, passes, driver);
return DrawWalkExitPortalMasks(frame, passes, driver);
}
private void ClearWalkFrameBindings()
@ -630,11 +660,12 @@ internal sealed class RetailPViewRenderer
/// portal is stamped once per exact walk-owned view captured for that
/// flood cell, matching retail's <c>CEnvCell::setup_view</c> loop. The
/// legacy visibility assembly has no production role here.</summary>
private void DrawWalkExitPortalMasks(
private int DrawWalkExitPortalMasks(
RetailPViewFrameInput ctx,
RetailPViewPassExecutor passes,
Walk.WalkFrameDriver driver)
{
int submitted = 0;
List<uint> floodCells = driver.InteriorFloodCells;
for (int i = floodCells.Count - 1; i >= 0; i--)
{
@ -642,12 +673,13 @@ internal sealed class RetailPViewRenderer
int sliceCount = driver.InteriorFloodViewSliceCountAt(i);
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
{
passes.DrawExitPortalMask(
submitted += passes.DrawExitPortalMask(
ctx,
cellId,
driver.InteriorFloodViewClipPlanesAt(i, sliceIndex));
}
}
return submitted;
}
private static RenderFrameDiagnosticCounts WalkDiagnosticCounts(

View file

@ -109,12 +109,14 @@ public sealed class RetailFrameWalk
/// <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>
/// retail has drawn anything), but retail itself draws the landscape
/// flush/stamp/gated-clear/seal turn (S3 §8.1 R3, all four strictly
/// inside <c>if (outside_view.view_count &gt; 0)</c>) and ONLY THEN the
/// flood's own cells far-to-near.
/// <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/> fires at that
/// later point, carrying the SAME <c>outside_view.view_count</c> the DC
/// event recorded, since retail's real gate reads it there (see that
/// method's own doc comment).</summary>
public void DrawInside(
WalkCell cell, WalkLandscape landscape,
IRetailFrameWalkContext ctx, IWalkEventSink sink)
@ -128,14 +130,15 @@ public sealed class RetailFrameWalk
_interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
uint[] floodCells = EmitDrawCells(_interiorPView, sink);
if (_interiorPView.OutsideView.ViewCount > 0)
int outsideViewCount = _interiorPView.OutsideView.ViewCount;
if (outsideViewCount > 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);
// the flood's actual cell-drawing turn. S3 §8.1 R3: outsideViewCount
// also gates the sink's own landscape-flush/stamp/clear/seal turn
// (ov==0 skips straight to the flood's own cells).
sink.OnInteriorFloodDrawTurn(floodCells, outsideViewCount);
RemoveViews(cell.StabList, ctx);
cell.PopView();

View file

@ -169,23 +169,31 @@ public interface IWalkEventSink
/// @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 in two reverse passes: every EnvCell shell first,
/// then every cell object list (the same <c>PView::DrawCells</c>
/// discipline used by 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.
/// the FW0 oracle traces; it only RECORDS the flood list). <paramref
/// name="outsideViewCount"/> is the SAME <c>outside_view.view_count</c>
/// the DC event's own <see cref="WalkEvent.OutsideViewCount"/> carries —
/// forwarded again here because retail's actual draw-time gate reads it
/// at THIS later point, not at the earlier record-only DC event.
/// Retail's own order inside <c>DrawCells</c> (0x005a4852-0x005a49eb,
/// S3 §8.1 R3) is: <c>LScape::draw</c> (pc:432719), then
/// <c>FlushAlphaList(0f)</c> @0x005a4872 plus the pre-clear dynamics
/// hook, then the device-stamp advance @0x005a4886, then a gated full
/// depth clear (pc:432731-432732, gated on the persistent
/// <c>portalsDrawnCount</c> counter read-then-zeroed at
/// @0x005a489c-0x005a489e — R4), then the exit-portal seals
/// (pc:432785-432786) — ALL FOUR sit strictly INSIDE
/// <c>if (outside_view.view_count &gt; 0)</c>; when
/// <paramref name="outsideViewCount"/> is 0 none of them run at all —
/// and ONLY THEN the flood's cells draw in two reverse passes: every
/// EnvCell shell first, then every cell object list (the same
/// <c>PView::DrawCells</c> discipline used by 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) { }
void OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells, int outsideViewCount) { }
}

View file

@ -142,30 +142,51 @@ internal interface IWalkFrameLeafRenderer
/// (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>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).</summary>
/// 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"/>, 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>/
/// <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. 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();
/// 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.
@ -261,6 +282,9 @@ internal enum WalkFrameEventKind : byte
/// 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,
@ -398,6 +422,9 @@ internal readonly struct WalkFrameEvent
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);
@ -446,30 +473,34 @@ internal readonly struct WalkFrameEvent
///
/// <para><b>The one mark rule that reproduces the whole frame script:</b>
/// before EVERY leaf-renderer event (<see cref="WalkFrameEventKind.Sky"/>,
/// <c>TerrainSlice</c>, <c>CellShell</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),
/// <c>TerrainSlice</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 → 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: [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)] [interior depth clear] [exit-portal seals] [the interior
/// root's own flood cells], and a final mark at Replay's prepare step. No
/// special-casing per turn kind is needed beyond that.</para>
/// 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
/// interior root's own flood cells] — the last four only when
/// <c>outside_view.view_count &gt; 0</c> (S3 §8.1 R3) — 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),
@ -522,6 +553,21 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
// 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).</summary>
internal int PortalsDrawnCount;
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
@ -594,7 +640,15 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
private IWalkBuildingFrameContext? _ctx;
private Matrix4x4 _viewProjection;
private Vector3 _cameraWorldPosition;
private bool _skyDrawnThisFrame;
/// <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;
@ -638,7 +692,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_ctx = null;
_viewProjection = default;
_cameraWorldPosition = default;
_skyDrawnThisFrame = false;
_landscapeTurnsThisFrame = 0;
_currentDcStage = null;
_readyToReplay = false;
_stream.Reset();
@ -774,7 +828,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
_ctx = ctx;
_viewProjection = viewProjection;
_cameraWorldPosition = cameraWorldPosition;
_skyDrawnThisFrame = false;
_landscapeTurnsThisFrame = 0;
_currentDcStage = null;
_readyToReplay = false;
_stream.Reset();
@ -893,11 +947,20 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
case WalkFrameEventKind.AlphaBarrier:
_leafRenderer.AlphaBarrier();
break;
case WalkFrameEventKind.LandscapeFlush:
_leafRenderer.FlushLandscape();
break;
case WalkFrameEventKind.ClearInteriorDepth:
_leafRenderer.ClearInteriorDepth();
break;
case WalkFrameEventKind.ExitSeals:
_leafRenderer.DrawExitSeals();
// 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:
// Retail CPhysicsObj::add_particle_shadow_to_cell
@ -1019,6 +1082,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
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;
@ -1233,39 +1299,62 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
WalkFrameEvent.PunchFan(TransformToWorld(polygon, worldTransform), activeViewIndex));
}
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells)
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells, int outsideViewCount)
{
ArgumentNullException.ThrowIfNull(cells);
RequireOpenFrame();
// PView::DrawCells @0x005a4840 advances m_nFrameStamp at 0x005a4886
// after LScape::draw + FlushAlphaList and before the depth clear. Its
// drawn-part AND DrawEnvCell dedupe is therefore 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.
MarkIfGrown();
if (_skyDrawnThisFrame)
// 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());
}
// 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).
_events.Add(WalkFrameEvent.ClearInteriorDepth());
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.
@ -1292,7 +1381,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
+ "surviving exit views, both at least 1). A zero/negative count is a "
+ "walk/driver desync (Campaign FW fail-loud rule).");
}
if (_skyDrawnThisFrame)
if (_landscapeTurnsThisFrame != 0)
{
throw new InvalidOperationException(
"A second Landscape turn fired in one frame — RetailFrameWalk.WalkFrame/"
@ -1305,7 +1394,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
MarkIfGrown();
_events.Add(WalkFrameEvent.Sky());
_skyDrawnThisFrame = true;
_landscapeTurnsThisFrame++;
// FW4 slice 6 (correcting slice 1's per-view fan): retail's
// LScape::draw draws the terrain blocks ONCE per landscape turn —
// the active views feed only the block-level visibility union