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:
parent
51d5323208
commit
be9b4c1f29
9 changed files with 607 additions and 153 deletions
|
|
@ -191,9 +191,13 @@ internal sealed partial class RetailPViewPassExecutor
|
||||||
/// with <c>forceFarZ</c>, clipped by the pinned view's slice planes
|
/// 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
|
/// (retail <c>building_view</c> @0x0059f3bf); FW3.3 draws fans at the dat
|
||||||
/// aperture verbatim (the ShellDrawLiftZ retirement).</item>
|
/// aperture verbatim (the ShellDrawLiftZ retirement).</item>
|
||||||
/// <item><see cref="ClearInteriorDepth"/>/<see cref="DrawExitSeals"/> →
|
/// <item><see cref="FlushLandscape"/>/<see cref="ClearInteriorDepth"/>/
|
||||||
/// caller-supplied actions (the renderer owns the pass scope and the
|
/// <see cref="DrawExitSeals"/> → caller-supplied delegates (the renderer
|
||||||
/// root-flood seal iteration; the adapter only provides the turns).</item>
|
/// 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
|
/// <item><see cref="AlphaBarrier"/> → <c>FlushLandscapeAlpha</c>, retail's
|
||||||
/// flush-all <c>FlushAlphaList(0f)</c> @0x0059f30b.</item>
|
/// flush-all <c>FlushAlphaList(0f)</c> @0x0059f30b.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
|
|
@ -203,8 +207,9 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
private RetailPViewPassExecutor _passes = null!;
|
private RetailPViewPassExecutor _passes = null!;
|
||||||
private RetailPViewFrameInput _frame = null!;
|
private RetailPViewFrameInput _frame = null!;
|
||||||
private ClipFrameAssembly _clipAssembly = null!;
|
private ClipFrameAssembly _clipAssembly = null!;
|
||||||
|
private Action _flushLandscape = null!;
|
||||||
private Action _clearInteriorDepth = null!;
|
private Action _clearInteriorDepth = null!;
|
||||||
private Action _drawExitSeals = null!;
|
private Func<int> _drawExitSeals = null!;
|
||||||
private readonly HashSet<uint> _singleCellScratch = new();
|
private readonly HashSet<uint> _singleCellScratch = new();
|
||||||
private readonly List<uint> _singleCellListScratch = new();
|
private readonly List<uint> _singleCellListScratch = new();
|
||||||
|
|
||||||
|
|
@ -212,9 +217,10 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
RetailPViewPassExecutor passes,
|
RetailPViewPassExecutor passes,
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
|
Action flushLandscape,
|
||||||
Action clearInteriorDepth,
|
Action clearInteriorDepth,
|
||||||
Action drawExitSeals)
|
Func<int> drawExitSeals)
|
||||||
=> Reset(passes, frame, clipAssembly, clearInteriorDepth, drawExitSeals);
|
=> Reset(passes, frame, clipAssembly, flushLandscape, clearInteriorDepth, drawExitSeals);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FW6 allocation closeout: bind the retained leaf and its retained
|
/// FW6 allocation closeout: bind the retained leaf and its retained
|
||||||
|
|
@ -225,13 +231,16 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
RetailPViewPassExecutor passes,
|
RetailPViewPassExecutor passes,
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
ClipFrameAssembly clipAssembly,
|
ClipFrameAssembly clipAssembly,
|
||||||
|
Action flushLandscape,
|
||||||
Action clearInteriorDepth,
|
Action clearInteriorDepth,
|
||||||
Action drawExitSeals)
|
Func<int> drawExitSeals)
|
||||||
{
|
{
|
||||||
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
||||||
_frame = frame ?? throw new ArgumentNullException(nameof(frame));
|
_frame = frame ?? throw new ArgumentNullException(nameof(frame));
|
||||||
_clipAssembly = clipAssembly
|
_clipAssembly = clipAssembly
|
||||||
?? throw new ArgumentNullException(nameof(clipAssembly));
|
?? throw new ArgumentNullException(nameof(clipAssembly));
|
||||||
|
_flushLandscape = flushLandscape
|
||||||
|
?? throw new ArgumentNullException(nameof(flushLandscape));
|
||||||
_clearInteriorDepth = clearInteriorDepth
|
_clearInteriorDepth = clearInteriorDepth
|
||||||
?? throw new ArgumentNullException(nameof(clearInteriorDepth));
|
?? throw new ArgumentNullException(nameof(clearInteriorDepth));
|
||||||
_drawExitSeals = drawExitSeals
|
_drawExitSeals = drawExitSeals
|
||||||
|
|
@ -272,6 +281,8 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void FlushLandscape() => _flushLandscape();
|
||||||
|
|
||||||
public void ClearInteriorDepth() => _clearInteriorDepth();
|
public void ClearInteriorDepth() => _clearInteriorDepth();
|
||||||
|
|
||||||
public void DrawStaticParticles(uint cellId) =>
|
public void DrawStaticParticles(uint cellId) =>
|
||||||
|
|
@ -280,7 +291,7 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
public void DrawCellParticles(uint cellId) =>
|
public void DrawCellParticles(uint cellId) =>
|
||||||
_passes.DrawCellParticles(_frame, cellId);
|
_passes.DrawCellParticles(_frame, cellId);
|
||||||
|
|
||||||
public void DrawExitSeals() => _drawExitSeals();
|
public int DrawExitSeals() => _drawExitSeals();
|
||||||
|
|
||||||
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
||||||
_passes.DrawWalkPunchFan(_frame, _clipAssembly, worldPolygon, activeViewIndex);
|
_passes.DrawWalkPunchFan(_frame, _clipAssembly, worldPolygon, activeViewIndex);
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,11 @@ public RetailPViewPassExecutor(
|
||||||
_surface.ClearInteriorDepth();
|
_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,
|
RetailPViewFrameInput frame,
|
||||||
uint cellId,
|
uint cellId,
|
||||||
ReadOnlySpan<Vector4> clipPlanes) =>
|
ReadOnlySpan<Vector4> clipPlanes) =>
|
||||||
|
|
@ -376,29 +380,34 @@ public RetailPViewPassExecutor(
|
||||||
frame.PlayerViewPosition,
|
frame.PlayerViewPosition,
|
||||||
frame.CameraCellResolution);
|
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 >=3 vertices —
|
||||||
|
/// the same enumeration <c>WalkFrameDriver.OnInteriorFloodDrawTurn</c>
|
||||||
|
/// counts through <c>DrawExitSeals</c>'s return value.</summary>
|
||||||
|
private int DrawPortalDepthWrite(
|
||||||
uint cellId,
|
uint cellId,
|
||||||
ReadOnlySpan<Vector4> clipPlanes,
|
ReadOnlySpan<Vector4> clipPlanes,
|
||||||
RetailPViewFrameInput frame,
|
RetailPViewFrameInput frame,
|
||||||
bool forceFarZ,
|
bool forceFarZ,
|
||||||
int? onlyPortalIndex = null)
|
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)
|
if (_portalDepthMask is null)
|
||||||
return;
|
return 0;
|
||||||
if (!forceFarZ
|
if (!forceFarZ
|
||||||
&& AcDream.Core.Rendering.RenderingDiagnostics
|
&& AcDream.Core.Rendering.RenderingDiagnostics
|
||||||
.ProbeCathedralSkipFloatingStairSeals
|
.ProbeCathedralSkipFloatingStairSeals
|
||||||
&& cellId is 0xF4180107u or 0xF4180112u)
|
&& cellId is 0xF4180107u or 0xF4180112u)
|
||||||
{
|
{
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
LoadedCell? cell = frame.Cells.Find(cellId);
|
LoadedCell? cell = frame.Cells.Find(cellId);
|
||||||
if (cell is null)
|
if (cell is null)
|
||||||
return;
|
return 0;
|
||||||
|
|
||||||
|
int submitted = 0;
|
||||||
Span<Vector3> world = stackalloc Vector3[32];
|
Span<Vector3> world = stackalloc Vector3[32];
|
||||||
for (int index = 0; index < cell.Portals.Count; index++)
|
for (int index = 0; index < cell.Portals.Count; index++)
|
||||||
{
|
{
|
||||||
|
|
@ -435,7 +444,9 @@ public RetailPViewPassExecutor(
|
||||||
frame.ViewProjection,
|
frame.ViewProjection,
|
||||||
clipPlanes,
|
clipPlanes,
|
||||||
forceFarZ);
|
forceFarZ);
|
||||||
|
submitted++;
|
||||||
}
|
}
|
||||||
|
return submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool BeginDoorwayScissor(Vector4 ndcAabb) =>
|
private bool BeginDoorwayScissor(Vector4 ndcAabb) =>
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,9 @@ internal sealed class RetailPViewRenderer
|
||||||
private Walk.WalkFrameDriver? _walkFrameDriverScratch;
|
private Walk.WalkFrameDriver? _walkFrameDriverScratch;
|
||||||
private Walk.WalkProductionFrameContext? _walkFrameContextScratch;
|
private Walk.WalkProductionFrameContext? _walkFrameContextScratch;
|
||||||
private WalkProductionLeafRenderer? _walkLeafRendererScratch;
|
private WalkProductionLeafRenderer? _walkLeafRendererScratch;
|
||||||
|
private readonly Action _walkFlushLandscapeAction;
|
||||||
private readonly Action _walkClearInteriorDepthAction;
|
private readonly Action _walkClearInteriorDepthAction;
|
||||||
private readonly Action _walkDrawExitSealsAction;
|
private readonly Func<int> _walkDrawExitSealsFunc;
|
||||||
private RetailPViewPassExecutor? _activeWalkPasses;
|
private RetailPViewPassExecutor? _activeWalkPasses;
|
||||||
private RetailPViewFrameInput? _activeWalkFrame;
|
private RetailPViewFrameInput? _activeWalkFrame;
|
||||||
private ClipFrameAssembly? _activeWalkClipAssembly;
|
private ClipFrameAssembly? _activeWalkClipAssembly;
|
||||||
|
|
@ -84,8 +85,9 @@ internal sealed class RetailPViewRenderer
|
||||||
_walkWorldData = new Walk.WalkProductionWorldData(
|
_walkWorldData = new Walk.WalkProductionWorldData(
|
||||||
_walkBuildings,
|
_walkBuildings,
|
||||||
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
shadows ?? throw new ArgumentNullException(nameof(shadows)));
|
||||||
|
_walkFlushLandscapeAction = FlushWalkLandscape;
|
||||||
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
_walkClearInteriorDepthAction = ClearWalkInteriorDepth;
|
||||||
_walkDrawExitSealsAction = DrawWalkExitSeals;
|
_walkDrawExitSealsFunc = DrawWalkExitSeals;
|
||||||
}
|
}
|
||||||
|
|
||||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||||
|
|
@ -219,8 +221,9 @@ internal sealed class RetailPViewRenderer
|
||||||
walkExecutor,
|
walkExecutor,
|
||||||
ctx,
|
ctx,
|
||||||
clipAssembly,
|
clipAssembly,
|
||||||
|
_walkFlushLandscapeAction,
|
||||||
_walkClearInteriorDepthAction,
|
_walkClearInteriorDepthAction,
|
||||||
_walkDrawExitSealsAction);
|
_walkDrawExitSealsFunc);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -228,8 +231,9 @@ internal sealed class RetailPViewRenderer
|
||||||
walkExecutor,
|
walkExecutor,
|
||||||
ctx,
|
ctx,
|
||||||
clipAssembly,
|
clipAssembly,
|
||||||
|
_walkFlushLandscapeAction,
|
||||||
_walkClearInteriorDepthAction,
|
_walkClearInteriorDepthAction,
|
||||||
_walkDrawExitSealsAction);
|
_walkDrawExitSealsFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_walkFrameDriverScratch is null)
|
if (_walkFrameDriverScratch is null)
|
||||||
|
|
@ -496,7 +500,15 @@ internal sealed class RetailPViewRenderer
|
||||||
|
|
||||||
private readonly Walk.RetailFrameWalk _frameWalk = new();
|
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>0</c>) — see
|
||||||
|
/// <see cref="Walk.IWalkFrameLeafRenderer.FlushLandscape"/>.</summary>
|
||||||
|
private void FlushWalkLandscape()
|
||||||
{
|
{
|
||||||
RetailPViewPassExecutor passes = _activeWalkPasses
|
RetailPViewPassExecutor passes = _activeWalkPasses
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
|
|
@ -507,10 +519,28 @@ internal sealed class RetailPViewRenderer
|
||||||
// LScape::draw, before the clear+seals.
|
// LScape::draw, before the clear+seals.
|
||||||
_walkPreClearDynamics?.Invoke();
|
_walkPreClearDynamics?.Invoke();
|
||||||
passes.FlushLandscapeAlpha();
|
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();
|
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>0</c> interior-root
|
||||||
|
/// flood's clear decision.</summary>
|
||||||
|
private int DrawWalkExitSeals()
|
||||||
{
|
{
|
||||||
RetailPViewFrameInput frame = _activeWalkFrame
|
RetailPViewFrameInput frame = _activeWalkFrame
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
|
|
@ -522,7 +552,7 @@ internal sealed class RetailPViewRenderer
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
"The retained walk leaf has no active driver binding.");
|
"The retained walk leaf has no active driver binding.");
|
||||||
|
|
||||||
DrawWalkExitPortalMasks(frame, passes, driver);
|
return DrawWalkExitPortalMasks(frame, passes, driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearWalkFrameBindings()
|
private void ClearWalkFrameBindings()
|
||||||
|
|
@ -630,11 +660,12 @@ internal sealed class RetailPViewRenderer
|
||||||
/// portal is stamped once per exact walk-owned view captured for that
|
/// portal is stamped once per exact walk-owned view captured for that
|
||||||
/// flood cell, matching retail's <c>CEnvCell::setup_view</c> loop. The
|
/// flood cell, matching retail's <c>CEnvCell::setup_view</c> loop. The
|
||||||
/// legacy visibility assembly has no production role here.</summary>
|
/// legacy visibility assembly has no production role here.</summary>
|
||||||
private void DrawWalkExitPortalMasks(
|
private int DrawWalkExitPortalMasks(
|
||||||
RetailPViewFrameInput ctx,
|
RetailPViewFrameInput ctx,
|
||||||
RetailPViewPassExecutor passes,
|
RetailPViewPassExecutor passes,
|
||||||
Walk.WalkFrameDriver driver)
|
Walk.WalkFrameDriver driver)
|
||||||
{
|
{
|
||||||
|
int submitted = 0;
|
||||||
List<uint> floodCells = driver.InteriorFloodCells;
|
List<uint> floodCells = driver.InteriorFloodCells;
|
||||||
for (int i = floodCells.Count - 1; i >= 0; i--)
|
for (int i = floodCells.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
|
|
@ -642,12 +673,13 @@ internal sealed class RetailPViewRenderer
|
||||||
int sliceCount = driver.InteriorFloodViewSliceCountAt(i);
|
int sliceCount = driver.InteriorFloodViewSliceCountAt(i);
|
||||||
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
|
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
|
||||||
{
|
{
|
||||||
passes.DrawExitPortalMask(
|
submitted += passes.DrawExitPortalMask(
|
||||||
ctx,
|
ctx,
|
||||||
cellId,
|
cellId,
|
||||||
driver.InteriorFloodViewClipPlanesAt(i, sliceIndex));
|
driver.InteriorFloodViewClipPlanesAt(i, sliceIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RenderFrameDiagnosticCounts WalkDiagnosticCounts(
|
private static RenderFrameDiagnosticCounts WalkDiagnosticCounts(
|
||||||
|
|
|
||||||
|
|
@ -109,12 +109,14 @@ public sealed class RetailFrameWalk
|
||||||
/// <see cref="WalkEventKind.DrawCells"/> <see cref="IWalkEventSink.Emit"/>
|
/// <see cref="WalkEventKind.DrawCells"/> <see cref="IWalkEventSink.Emit"/>
|
||||||
/// call below fires at breakpoint-ENTRY order (matching the FW0 oracle
|
/// call below fires at breakpoint-ENTRY order (matching the FW0 oracle
|
||||||
/// traces, whose breakpoint sat at <c>DrawCells</c> entry — before
|
/// traces, whose breakpoint sat at <c>DrawCells</c> entry — before
|
||||||
/// retail has drawn anything), but retail itself draws
|
/// retail has drawn anything), but retail itself draws the landscape
|
||||||
/// <c>LScape::draw</c> FIRST (pc:432719, only when exit views survived),
|
/// flush/stamp/gated-clear/seal turn (S3 §8.1 R3, all four strictly
|
||||||
/// then a gated full depth clear (pc:432731-432732), then the exit-
|
/// inside <c>if (outside_view.view_count > 0)</c>) and ONLY THEN the
|
||||||
/// portal seals (pc:432785-432786), and ONLY THEN the flood's own cells
|
/// flood's own cells far-to-near.
|
||||||
/// far-to-near. <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>
|
/// <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/> fires at that
|
||||||
/// fires at that later point (see its own doc comment).</summary>
|
/// 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(
|
public void DrawInside(
|
||||||
WalkCell cell, WalkLandscape landscape,
|
WalkCell cell, WalkLandscape landscape,
|
||||||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||||
|
|
@ -128,14 +130,15 @@ public sealed class RetailFrameWalk
|
||||||
_interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
|
_interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
|
||||||
|
|
||||||
uint[] floodCells = EmitDrawCells(_interiorPView, sink);
|
uint[] floodCells = EmitDrawCells(_interiorPView, sink);
|
||||||
if (_interiorPView.OutsideView.ViewCount > 0)
|
int outsideViewCount = _interiorPView.OutsideView.ViewCount;
|
||||||
|
if (outsideViewCount > 0)
|
||||||
DrawLandscape(landscape, _interiorPView.OutsideView, ctx, sink);
|
DrawLandscape(landscape, _interiorPView.OutsideView, ctx, sink);
|
||||||
|
|
||||||
// Additive (Campaign FW3.2b-1): see this method's own doc comment —
|
// Additive (Campaign FW3.2b-1): see this method's own doc comment —
|
||||||
// the flood's actual cell-drawing turn, unconditional of whether a
|
// the flood's actual cell-drawing turn. S3 §8.1 R3: outsideViewCount
|
||||||
// landscape turn just ran (ov==0 skips straight here from the DC
|
// also gates the sink's own landscape-flush/stamp/clear/seal turn
|
||||||
// event above).
|
// (ov==0 skips straight to the flood's own cells).
|
||||||
sink.OnInteriorFloodDrawTurn(floodCells);
|
sink.OnInteriorFloodDrawTurn(floodCells, outsideViewCount);
|
||||||
|
|
||||||
RemoveViews(cell.StabList, ctx);
|
RemoveViews(cell.StabList, ctx);
|
||||||
cell.PopView();
|
cell.PopView();
|
||||||
|
|
|
||||||
|
|
@ -169,23 +169,31 @@ public interface IWalkEventSink
|
||||||
/// @0x005a4840 actually DRAWS the root flood's own cells — NOT where the
|
/// @0x005a4840 actually DRAWS the root flood's own cells — NOT where the
|
||||||
/// <see cref="WalkEventKind.DrawCells"/> <see cref="Emit"/> call for the
|
/// <see cref="WalkEventKind.DrawCells"/> <see cref="Emit"/> call for the
|
||||||
/// SAME flood fires (that one sits at breakpoint-ENTRY order, matching
|
/// SAME flood fires (that one sits at breakpoint-ENTRY order, matching
|
||||||
/// the FW0 oracle traces; it only RECORDS the flood list). Retail's own
|
/// the FW0 oracle traces; it only RECORDS the flood list). <paramref
|
||||||
/// order inside <c>DrawCells</c> is: <c>LScape::draw</c> FIRST
|
/// name="outsideViewCount"/> is the SAME <c>outside_view.view_count</c>
|
||||||
/// (pc:432719, only when exit views survived — see
|
/// the DC event's own <see cref="WalkEvent.OutsideViewCount"/> carries —
|
||||||
/// <see cref="RetailFrameWalk.DrawLandscape"/> and
|
/// forwarded again here because retail's actual draw-time gate reads it
|
||||||
/// <see cref="WalkEventKind.Landscape"/>), then a full depth clear
|
/// at THIS later point, not at the earlier record-only DC event.
|
||||||
/// (pc:432731-432732), then the exit-portal seals (pc:432785-432786) —
|
/// Retail's own order inside <c>DrawCells</c> (0x005a4852-0x005a49eb,
|
||||||
/// BOTH unconditional for an interior root's
|
/// S3 §8.1 R3) is: <c>LScape::draw</c> (pc:432719), then
|
||||||
/// own flood, whether or not a landscape turn just ran — and ONLY THEN
|
/// <c>FlushAlphaList(0f)</c> @0x005a4872 plus the pre-clear dynamics
|
||||||
/// the flood's cells in two reverse passes: every EnvCell shell first,
|
/// hook, then the device-stamp advance @0x005a4886, then a gated full
|
||||||
/// then every cell object list (the same <c>PView::DrawCells</c>
|
/// depth clear (pc:432731-432732, gated on the persistent
|
||||||
/// discipline used by a building's look-in). This hook fires at that later point, so
|
/// <c>portalsDrawnCount</c> counter read-then-zeroed at
|
||||||
/// this is where a driver should actually draw <paramref name="cells"/>.
|
/// @0x005a489c-0x005a489e — R4), then the exit-portal seals
|
||||||
/// Building look-in floods are UNAFFECTED — retail calls
|
/// (pc:432785-432786) — ALL FOUR sit strictly INSIDE
|
||||||
/// <c>DrawCells</c> re-entrantly there with <c>ov==0</c> and no
|
/// <c>if (outside_view.view_count > 0)</c>; when
|
||||||
/// landscape/clear/seal step, so their <see cref="WalkEventKind.DrawCells"/>
|
/// <paramref name="outsideViewCount"/> is 0 none of them run at all —
|
||||||
/// <see cref="Emit"/> call still fires at the actual draw point (a
|
/// and ONLY THEN the flood's cells draw in two reverse passes: every
|
||||||
/// driver may keep drawing those immediately, as before). Default no-op.
|
/// 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>
|
/// </summary>
|
||||||
void OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells) { }
|
void OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells, int outsideViewCount) { }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,30 +142,51 @@ internal interface IWalkFrameLeafRenderer
|
||||||
/// (pc:432731-432732) between the outside stage and the interior root's
|
/// (pc:432731-432732) between the outside stage and the interior root's
|
||||||
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
|
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
|
||||||
/// (see that interface's own member of the same name in
|
/// (see that interface's own member of the same name in
|
||||||
/// <c>RetailPViewRenderer.cs</c>, staged there on <c>OutsideViewSlices.Length
|
/// <c>RetailPViewRenderer.cs</c>). The driver calls this leaf ONLY when
|
||||||
/// > 0</c> — an ACKNOWLEDGED approximation of retail's true
|
/// <see cref="WalkFrameDriver.PortalsDrawnCount"/> was nonzero at the
|
||||||
/// <c>portalsDrawnCount</c> gate per that file's own comment). This walk
|
/// read-then-zero decision point (S3 §8.1 R4: retail's
|
||||||
/// driver instead fires unconditionally for every interior root (per the
|
/// <c>portalsDrawnCount</c>, read-then-zeroed @0x005a489c-0x005a489e —
|
||||||
/// 2026-08-30 decomp correction: the coordinator's directive supersedes
|
/// <c>forceClear</c> never writes in the pseudo-C, so the clear fires
|
||||||
/// the packed path's staged gate — reconcile the two if a firmer
|
/// iff the counter was nonzero). Only called for an INTERIOR root, never
|
||||||
/// <c>portalsDrawnCount</c> reading ever lands). Only called for an
|
/// outdoors (retail has no depth clear there —
|
||||||
/// INTERIOR root, never outdoors (retail has no depth clear there —
|
|
||||||
/// <c>portalsDrawnCount</c> never applies to <c>LScape::draw</c>'s own
|
/// <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();
|
void ClearInteriorDepth();
|
||||||
|
|
||||||
/// <summary>The exit-portal seals (pc:432785-432786) — re-stamping every
|
/// <summary>The exit-portal seals (pc:432785-432786) — re-stamping every
|
||||||
/// outside-leading portal's TRUE depth right after
|
/// outside-leading portal's TRUE depth right after
|
||||||
/// <see cref="ClearInteriorDepth"/>, so the aperture the clear just wiped
|
/// <see cref="ClearInteriorDepth"/> (when it ran) so the aperture the
|
||||||
/// stays occluded by the world beyond it rather than by whatever draws
|
/// clear just wiped stays occluded by the world beyond it rather than by
|
||||||
/// next. Production maps this to the existing seal-fan machinery
|
/// whatever draws next. Production maps this to the existing seal-fan
|
||||||
/// (<c>RetailPViewRenderer.DrawExitPortalMask</c>/
|
/// machinery (<c>RetailPViewRenderer.DrawExitPortalMask</c>/
|
||||||
/// <c>PortalDepthMaskRenderer</c>) — this driver only provides the TURN;
|
/// <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
|
/// the real per-portal fan geometry is FW3.2b-2's job. Returns the
|
||||||
/// INTERIOR root's own flood, never for a building look-in (those call
|
/// number of seal polygons actually submitted this turn (S3 §8.1 R4/§8.2
|
||||||
/// <c>DrawCells</c> re-entrantly with no clear/seal step) and never
|
/// B2: retail's <c>D3DPolyRender::portalsDrawnCount</c> @0x008719b4
|
||||||
/// outdoors.</summary>
|
/// increments once per <c>DrawPortalPolyInternal</c> call with its
|
||||||
void DrawExitSeals();
|
/// 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>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 > 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>0</c>),
|
||||||
|
/// never outdoors and never for a building look-in.</summary>
|
||||||
|
void FlushLandscape();
|
||||||
|
|
||||||
/// <summary><c>DrawPortalPolyInternal</c> @0x0059bc90's depth-only far-Z
|
/// <summary><c>DrawPortalPolyInternal</c> @0x0059bc90's depth-only far-Z
|
||||||
/// punch fan — pass 1 of the building portal walk.
|
/// 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>
|
/// at Collect time (the context that supplies it does not outlive Collect).</summary>
|
||||||
AlphaBarrier,
|
AlphaBarrier,
|
||||||
|
|
||||||
|
/// <summary><see cref="IWalkFrameLeafRenderer.FlushLandscape"/>.</summary>
|
||||||
|
LandscapeFlush,
|
||||||
|
|
||||||
/// <summary><see cref="IWalkFrameLeafRenderer.ClearInteriorDepth"/>.</summary>
|
/// <summary><see cref="IWalkFrameLeafRenderer.ClearInteriorDepth"/>.</summary>
|
||||||
ClearInteriorDepth,
|
ClearInteriorDepth,
|
||||||
|
|
||||||
|
|
@ -398,6 +422,9 @@ internal readonly struct WalkFrameEvent
|
||||||
internal static WalkFrameEvent CellParticles(uint cellId) =>
|
internal static WalkFrameEvent CellParticles(uint cellId) =>
|
||||||
new(WalkFrameEventKind.CellParticles, 0, cellId, 0f, null);
|
new(WalkFrameEventKind.CellParticles, 0, cellId, 0f, null);
|
||||||
|
|
||||||
|
internal static WalkFrameEvent LandscapeFlush() =>
|
||||||
|
new(WalkFrameEventKind.LandscapeFlush, 0, 0, 0f, null);
|
||||||
|
|
||||||
internal static WalkFrameEvent ClearInteriorDepth() =>
|
internal static WalkFrameEvent ClearInteriorDepth() =>
|
||||||
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
|
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>
|
/// <para><b>The one mark rule that reproduces the whole frame script:</b>
|
||||||
/// before EVERY leaf-renderer event (<see cref="WalkFrameEventKind.Sky"/>,
|
/// before EVERY leaf-renderer event (<see cref="WalkFrameEventKind.Sky"/>,
|
||||||
/// <c>TerrainSlice</c>, <c>CellShell</c>, <c>ClearInteriorDepth</c>,
|
/// <c>TerrainSlice</c>, <c>CellShell</c>, <c>LandscapeFlush</c>,
|
||||||
/// <c>ExitSeals</c>, <c>PunchFan</c>) and before every
|
/// <c>ClearInteriorDepth</c>, <c>ExitSeals</c>, <c>PunchFan</c>) and before
|
||||||
/// <see cref="WalkFrameEventKind.AlphaBarrier"/> event, Collect records a
|
/// every <see cref="WalkFrameEventKind.AlphaBarrier"/> event, Collect
|
||||||
/// <see cref="WalkFrameEventKind.StreamMark"/> if the stream grew since the
|
/// records a <see cref="WalkFrameEventKind.StreamMark"/> if the stream grew
|
||||||
/// last one (a no-op otherwise — "empty segments submit nothing"); a
|
/// since the last one (a no-op otherwise — "empty segments submit
|
||||||
/// building's own shell content is APPENDED (not marked) the moment
|
/// nothing"); a building's own shell content is APPENDED (not marked) the
|
||||||
/// <see cref="IWalkEventSink.OnBuildingShellTurn"/> fires, so it only gets a
|
/// moment <see cref="IWalkEventSink.OnBuildingShellTurn"/> fires, so it only
|
||||||
/// mark ahead of whatever non-stream event comes next (the next building's
|
/// gets a mark ahead of whatever non-stream event comes next (the next
|
||||||
/// alpha barrier, or the final mark at <see cref="Replay"/>'s prepare step).
|
/// building's alpha barrier, or the final mark at <see cref="Replay"/>'s
|
||||||
/// This single rule, combined with retail's two reverse flood passes (ALL
|
/// prepare step). This single rule, combined with retail's two reverse
|
||||||
/// shells, then ALL contents),
|
/// flood passes (ALL shells, then ALL contents),
|
||||||
/// retail's own building order (alpha barrier → portal pass → shell — see
|
/// retail's own building order (alpha barrier → portal pass → shell — see
|
||||||
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
|
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
|
||||||
/// own interior-root DRAW order (landscape → clear → seals → the flood's own
|
/// own interior-root DRAW order (landscape → flush/stamp/[gated clear]/seals
|
||||||
/// cells — see <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>'s doc
|
/// → the flood's own cells, the middle four steps ALL gated on
|
||||||
/// comment; this is NOT the order the walk's EVENTS fire in, which is
|
/// <c>outside_view.view_count > 0</c> — see
|
||||||
/// breakpoint-entry order matching the FW0 oracle traces), is what produces
|
/// <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>'s doc comment; this
|
||||||
/// every ordering constraint the plan's frame script names: [far shell] …
|
/// is NOT the order the walk's EVENTS fire in, which is breakpoint-entry
|
||||||
/// [near shell] [far contents] … [near contents], [alpha barrier] [punch
|
/// order matching the FW0 oracle traces), is what produces every ordering
|
||||||
/// fan(s) + look-in flood(s), each following the SAME reverse two-pass
|
/// constraint the plan's frame script names: [far shell] … [near shell]
|
||||||
/// discipline] [building shell content mark], [landscape (if exit views
|
/// [far contents] … [near contents], [alpha barrier] [punch fan(s) +
|
||||||
/// survived)] [interior depth clear] [exit-portal seals] [the interior
|
/// look-in flood(s), each following the SAME reverse two-pass discipline]
|
||||||
/// root's own flood cells], and a final mark at Replay's prepare step. No
|
/// [building shell content mark], [landscape (if exit views survived)]
|
||||||
/// special-casing per turn kind is needed beyond that.</para>
|
/// [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 > 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
|
/// <para>Retail anchors: <c>SmartBox::RenderNormalMode</c> @0x00453aa0 (the
|
||||||
/// root <see cref="RetailFrameWalk.WalkFrame"/> already ports),
|
/// 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).
|
// Chunk 6 review F1: one particle turn per cell per render stamp (see EmitCellContentsTurn).
|
||||||
private readonly HashSet<uint> _cellParticleTurnsDrawnThisFrame = new();
|
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>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>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;
|
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
|
||||||
|
|
||||||
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
|
/// <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 IWalkBuildingFrameContext? _ctx;
|
||||||
private Matrix4x4 _viewProjection;
|
private Matrix4x4 _viewProjection;
|
||||||
private Vector3 _cameraWorldPosition;
|
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 WalkDrawStage? _currentDcStage;
|
||||||
private bool _readyToReplay;
|
private bool _readyToReplay;
|
||||||
private int _cellViewRouteIndex;
|
private int _cellViewRouteIndex;
|
||||||
|
|
@ -638,7 +692,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
_ctx = null;
|
_ctx = null;
|
||||||
_viewProjection = default;
|
_viewProjection = default;
|
||||||
_cameraWorldPosition = default;
|
_cameraWorldPosition = default;
|
||||||
_skyDrawnThisFrame = false;
|
_landscapeTurnsThisFrame = 0;
|
||||||
_currentDcStage = null;
|
_currentDcStage = null;
|
||||||
_readyToReplay = false;
|
_readyToReplay = false;
|
||||||
_stream.Reset();
|
_stream.Reset();
|
||||||
|
|
@ -774,7 +828,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
_ctx = ctx;
|
_ctx = ctx;
|
||||||
_viewProjection = viewProjection;
|
_viewProjection = viewProjection;
|
||||||
_cameraWorldPosition = cameraWorldPosition;
|
_cameraWorldPosition = cameraWorldPosition;
|
||||||
_skyDrawnThisFrame = false;
|
_landscapeTurnsThisFrame = 0;
|
||||||
_currentDcStage = null;
|
_currentDcStage = null;
|
||||||
_readyToReplay = false;
|
_readyToReplay = false;
|
||||||
_stream.Reset();
|
_stream.Reset();
|
||||||
|
|
@ -893,11 +947,20 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
case WalkFrameEventKind.AlphaBarrier:
|
case WalkFrameEventKind.AlphaBarrier:
|
||||||
_leafRenderer.AlphaBarrier();
|
_leafRenderer.AlphaBarrier();
|
||||||
break;
|
break;
|
||||||
|
case WalkFrameEventKind.LandscapeFlush:
|
||||||
|
_leafRenderer.FlushLandscape();
|
||||||
|
break;
|
||||||
case WalkFrameEventKind.ClearInteriorDepth:
|
case WalkFrameEventKind.ClearInteriorDepth:
|
||||||
_leafRenderer.ClearInteriorDepth();
|
_leafRenderer.ClearInteriorDepth();
|
||||||
break;
|
break;
|
||||||
case WalkFrameEventKind.ExitSeals:
|
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;
|
break;
|
||||||
case WalkFrameEventKind.StaticParticles:
|
case WalkFrameEventKind.StaticParticles:
|
||||||
// Retail CPhysicsObj::add_particle_shadow_to_cell
|
// Retail CPhysicsObj::add_particle_shadow_to_cell
|
||||||
|
|
@ -1019,6 +1082,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
case WalkFrameEventKind.AlphaBarrier:
|
case WalkFrameEventKind.AlphaBarrier:
|
||||||
order.Append('>').Append(i).Append(":AB");
|
order.Append('>').Append(i).Append(":AB");
|
||||||
break;
|
break;
|
||||||
|
case WalkFrameEventKind.LandscapeFlush:
|
||||||
|
order.Append('>').Append(i).Append(":LF");
|
||||||
|
break;
|
||||||
case WalkFrameEventKind.ClearInteriorDepth:
|
case WalkFrameEventKind.ClearInteriorDepth:
|
||||||
order.Append('>').Append(i).Append(":CLEAR");
|
order.Append('>').Append(i).Append(":CLEAR");
|
||||||
break;
|
break;
|
||||||
|
|
@ -1233,39 +1299,62 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
WalkFrameEvent.PunchFan(TransformToWorld(polygon, worldTransform), activeViewIndex));
|
WalkFrameEvent.PunchFan(TransformToWorld(polygon, worldTransform), activeViewIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells)
|
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells, int outsideViewCount)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(cells);
|
ArgumentNullException.ThrowIfNull(cells);
|
||||||
RequireOpenFrame();
|
RequireOpenFrame();
|
||||||
|
|
||||||
// PView::DrawCells @0x005a4840 advances m_nFrameStamp at 0x005a4886
|
// PView::DrawCells @0x005a4840 (S3 §8.1 R3): the landscape flush, the
|
||||||
// after LScape::draw + FlushAlphaList and before the depth clear. Its
|
// device-stamp advance, the gated depth clear, and the exit-portal
|
||||||
// drawn-part AND DrawEnvCell dedupe is therefore per render stamp, not
|
// seals ALL sit strictly inside `if (outside_view.view_count > 0)`
|
||||||
// per presented frame: content admitted during the landscape must
|
// (0x005a4852-0x005a49eb) — outsideViewCount==0 skips straight to
|
||||||
// remain eligible for the interior-cell repaint after the clear. In
|
// the flood's own cells below with none of the four having run.
|
||||||
// Collect, every landscape candidate has been classified by this point
|
if (outsideViewCount > 0)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
|
// 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();
|
_dispatcher.AdvanceWalkPartPassStamp();
|
||||||
_cellShellsDrawnThisFrame.Clear();
|
_cellShellsDrawnThisFrame.Clear();
|
||||||
_cellParticleTurnsDrawnThisFrame.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
|
// FW4 slice 2: retain the ordered flood for the seal draw (the
|
||||||
// DrawExitSeals leaf runs at Replay, when Collect has long filled
|
// DrawExitSeals leaf runs at Replay, when Collect has long filled
|
||||||
// this) — see the property's own doc comment.
|
// 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 "
|
+ "surviving exit views, both at least 1). A zero/negative count is a "
|
||||||
+ "walk/driver desync (Campaign FW fail-loud rule).");
|
+ "walk/driver desync (Campaign FW fail-loud rule).");
|
||||||
}
|
}
|
||||||
if (_skyDrawnThisFrame)
|
if (_landscapeTurnsThisFrame != 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"A second Landscape turn fired in one frame — RetailFrameWalk.WalkFrame/"
|
"A second Landscape turn fired in one frame — RetailFrameWalk.WalkFrame/"
|
||||||
|
|
@ -1305,7 +1394,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
|
|
||||||
MarkIfGrown();
|
MarkIfGrown();
|
||||||
_events.Add(WalkFrameEvent.Sky());
|
_events.Add(WalkFrameEvent.Sky());
|
||||||
_skyDrawnThisFrame = true;
|
_landscapeTurnsThisFrame++;
|
||||||
// FW4 slice 6 (correcting slice 1's per-view fan): retail's
|
// FW4 slice 6 (correcting slice 1's per-view fan): retail's
|
||||||
// LScape::draw draws the terrain blocks ONCE per landscape turn —
|
// LScape::draw draws the terrain blocks ONCE per landscape turn —
|
||||||
// the active views feed only the block-level visibility union
|
// the active views feed only the block-level visibility union
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,27 @@ public sealed class RetailFrameWalkTests
|
||||||
Assert.Equal("DI:a9b40150|DC:ov=1:a9b40150|LS", recorder.Signature());
|
Assert.Equal("DI:a9b40150|DC:ov=1:a9b40150|LS", recorder.Signature());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── T4 / R1 (S3 chunk 2): RenderDeviceD3D::Init @0x0059efb0 constructs
|
||||||
|
// TWO PViews — indoor_pview = PView(…, 1) [draw_landscape=true] and
|
||||||
|
// outdoor_pview = PView(…, 0) [draw_landscape=false]. A building
|
||||||
|
// look-in's own DrawCells always runs through the OUTDOOR pview
|
||||||
|
// (RetailFrameWalk.DrawBuilding passes _outdoorPView into
|
||||||
|
// WalkBuildingPortals.DrawPortal), so it can never raise an outside
|
||||||
|
// view regardless of what portals the reached cell has — the mechanism
|
||||||
|
// itself (draw_landscape gating outside_view population) is pinned at
|
||||||
|
// the WalkPView level by
|
||||||
|
// WalkPViewFloodTests.Exit_portal_raises_the_outside_view_only_when_landscape_is_drawn;
|
||||||
|
// this test pins the ASSEMBLY fact that RetailFrameWalk wires the two
|
||||||
|
// PView instances with the correct flags.
|
||||||
|
[Fact]
|
||||||
|
public void RetailFrameWalk_WiresTheOutdoorPViewWithDrawLandscapeFalse_AndTheInteriorPViewTrue()
|
||||||
|
{
|
||||||
|
var walk = new RetailFrameWalk();
|
||||||
|
|
||||||
|
Assert.False(walk.OutdoorPView.DrawLandscape);
|
||||||
|
Assert.True(walk.InteriorPView.DrawLandscape);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Outdoor_camera_cell_roots_the_landscape_walk()
|
public void Outdoor_camera_cell_roots_the_landscape_walk()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,16 @@ public sealed class WalkFrameDriverTests
|
||||||
public readonly List<uint> Shells = new();
|
public readonly List<uint> Shells = new();
|
||||||
public readonly List<int> AlphaPendingAtBarrier = new();
|
public readonly List<int> AlphaPendingAtBarrier = new();
|
||||||
|
|
||||||
|
/// <summary>S3 chunk 2: the exit-seal polygon count this fake
|
||||||
|
/// reports back to the driver (B2 — <see cref="DrawExitSeals"/>
|
||||||
|
/// returns the submitted count so the driver can re-arm its
|
||||||
|
/// persistent <c>PortalsDrawnCount</c> for the NEXT <c>ov>0</c>
|
||||||
|
/// frame's clear decision, R4). Defaults to 1 so a test that
|
||||||
|
/// doesn't care about the re-arm mechanism sees an ordinary
|
||||||
|
/// "some seal fired" outcome; a test proving "flood with no exit
|
||||||
|
/// portal never clears" sets this to 0.</summary>
|
||||||
|
public int SealPolygonsSubmitted = 1;
|
||||||
|
|
||||||
public void DrawSky() => log.Add("SKY");
|
public void DrawSky() => log.Add("SKY");
|
||||||
|
|
||||||
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
|
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
|
||||||
|
|
@ -58,9 +68,15 @@ public sealed class WalkFrameDriverTests
|
||||||
log.Add($"SHELL:{cellId:x8}");
|
log.Add($"SHELL:{cellId:x8}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void FlushLandscape() => log.Add("LFLUSH");
|
||||||
|
|
||||||
public void ClearInteriorDepth() => log.Add("CLEAR");
|
public void ClearInteriorDepth() => log.Add("CLEAR");
|
||||||
|
|
||||||
public void DrawExitSeals() => log.Add("SEALS");
|
public int DrawExitSeals()
|
||||||
|
{
|
||||||
|
log.Add("SEALS");
|
||||||
|
return SealPolygonsSubmitted;
|
||||||
|
}
|
||||||
|
|
||||||
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex)
|
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex)
|
||||||
{
|
{
|
||||||
|
|
@ -189,19 +205,24 @@ public sealed class WalkFrameDriverTests
|
||||||
// PView::DrawCells' exact two reverse loops: ALL shells far-to-near,
|
// PView::DrawCells' exact two reverse loops: ALL shells far-to-near,
|
||||||
// then ALL object cells far-to-near. ─────────────────────────────────
|
// then ALL object cells far-to-near. ─────────────────────────────────
|
||||||
|
|
||||||
// ── Deliverable (2026-08-30 decomp correction): PView::DrawCells
|
// ── Deliverable (S3 chunk 2, superseding the 2026-08-30 decomp
|
||||||
// @0x005a4840's actual DRAW order for an interior root's OWN flood is
|
// correction): PView::DrawCells @0x005a4840's actual DRAW order for an
|
||||||
// NOT the order its DrawInside/DrawCells EVENTS fire in (breakpoint-
|
// interior root's OWN flood is NOT the order its DrawInside/DrawCells
|
||||||
// entry order, matching the FW0 oracle traces) — retail draws
|
// EVENTS fire in (breakpoint-entry order, matching the FW0 oracle
|
||||||
// LScape::draw FIRST (pc:432719, only when exit views survived), then
|
// traces) — retail draws LScape::draw FIRST (pc:432719, only when exit
|
||||||
// the depth clear (pc:432731-432732), then the exit-portal seals
|
// views survived), then the landscape flush + device-stamp advance,
|
||||||
// (pc:432785-432786), and ONLY THEN the flood's own cells far-to-near.
|
// then a GATED depth clear (pc:432731-432732 — R4: read-then-zero
|
||||||
// This case has a surviving exit view (ov=1): DC records the flood list
|
// portalsDrawnCount @0x005a489c-0x005a489e, clear iff nonzero), then
|
||||||
// (no draw), the landscape turn runs (flush no-op, sky, terrain), THEN
|
// the exit-portal seals (pc:432785-432786), and ONLY THEN the flood's
|
||||||
// clear, seals, then all shells and all contents in reverse order. ───
|
// own cells far-to-near. This case has a surviving exit view (ov=1): DC
|
||||||
|
// records the flood list (no draw), the landscape turn runs (sky,
|
||||||
|
// terrain), THEN flush, THEN — since this is a FRESH driver's very
|
||||||
|
// first ov>0 flood, so portalsDrawnCount starts at 0 (R4's
|
||||||
|
// "first-frame no-clear quirk") — the clear is SKIPPED, then seals,
|
||||||
|
// then all shells and all contents in reverse order. ──────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RunFrame_InteriorFloodWithExitView_DrawsLandscapeThenClearSealsThenFloodCells()
|
public void RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells()
|
||||||
{
|
{
|
||||||
using var fx = new DispatcherFixture();
|
using var fx = new DispatcherFixture();
|
||||||
var log = new List<string>();
|
var log = new List<string>();
|
||||||
|
|
@ -271,12 +292,19 @@ public sealed class WalkFrameDriverTests
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"SKY", "TERRAIN:0", "CLEAR", "SEALS",
|
// No CLEAR: R4's first-frame no-clear quirk — this driver's
|
||||||
|
// PortalsDrawnCount starts at 0, so the read-then-zero
|
||||||
|
// decision sees "not armed" even though ov=1.
|
||||||
|
"SKY", "TERRAIN:0", "LFLUSH", "SEALS",
|
||||||
"SHELL:00000101", "SHELL:00000100",
|
"SHELL:00000101", "SHELL:00000100",
|
||||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
|
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
|
||||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
|
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
|
||||||
},
|
},
|
||||||
log);
|
log);
|
||||||
|
Assert.DoesNotContain("CLEAR", log);
|
||||||
|
// This frame's own seals (RecordingLeafRenderer's default
|
||||||
|
// SealPolygonsSubmitted) armed the counter for a NEXT ov>0 frame.
|
||||||
|
Assert.Equal(1, driver.PortalsDrawnCount);
|
||||||
|
|
||||||
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
||||||
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||||
|
|
@ -346,7 +374,9 @@ public sealed class WalkFrameDriverTests
|
||||||
ctx.ViewportHeight);
|
ctx.ViewportHeight);
|
||||||
sink.OnLandscapeViews(landscapeViews);
|
sink.OnLandscapeViews(landscapeViews);
|
||||||
sink.OnLandscapeCellTurn(outdoorCellId);
|
sink.OnLandscapeCellTurn(outdoorCellId);
|
||||||
sink.OnInteriorFloodDrawTurn([interiorCellId]);
|
// A landscape turn ran (activeViewCount: 1 above), so this models
|
||||||
|
// retail's ov>0 case.
|
||||||
|
sink.OnInteriorFloodDrawTurn([interiorCellId], outsideViewCount: 1);
|
||||||
driver.EndFrame();
|
driver.EndFrame();
|
||||||
driver.Replay(draw.Frame, draw.Pass);
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
|
|
@ -359,13 +389,15 @@ public sealed class WalkFrameDriverTests
|
||||||
|| entry == "FLUSH:1:CellStatic"));
|
|| entry == "FLUSH:1:CellStatic"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Deliverable: the ov==0 interior case — no exit view survives, so
|
// ── Deliverable (T1, flipped for S3 chunk 2): the ov==0 interior case —
|
||||||
// DrawInside never runs the landscape turn at all; retail's clear+seals
|
// no exit view survives, so DrawInside never runs the landscape turn,
|
||||||
// still run unconditionally for the interior root's own flood, straight
|
// and PView::DrawCells' whole landscape-flush/stamp/clear/seal turn
|
||||||
// after the (draw-nothing) DC event. ───────────────────────────────────
|
// (S3 §8.1 R3: all four gated on outside_view.view_count > 0) never
|
||||||
|
// runs either — straight to the flood's own cells after the
|
||||||
|
// (draw-nothing) DC event. ──────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeButStillClearsAndSeals()
|
public void RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeAndNeverFlushesClearsOrSeals()
|
||||||
{
|
{
|
||||||
using var fx = new DispatcherFixture();
|
using var fx = new DispatcherFixture();
|
||||||
var log = new List<string>();
|
var log = new List<string>();
|
||||||
|
|
@ -416,15 +448,18 @@ public sealed class WalkFrameDriverTests
|
||||||
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
|
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
|
||||||
|
|
||||||
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
|
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
|
||||||
// at all — but CLEAR/SEALS still fire unconditionally.
|
// at all. T1 (flipped): LFLUSH/CLEAR/SEALS and the device-stamp
|
||||||
|
// advance are ALSO absent now — S3 §8.1 R3 gates all four on
|
||||||
|
// outside_view.view_count > 0, and it is 0 here.
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"CLEAR", "SEALS", "SHELL:00000101", "SHELL:00000100",
|
"SHELL:00000101", "SHELL:00000100",
|
||||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
|
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
|
||||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
|
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
|
||||||
},
|
},
|
||||||
log);
|
log);
|
||||||
|
Assert.Equal(0, driver.PortalsDrawnCount);
|
||||||
|
|
||||||
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
||||||
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||||
|
|
@ -434,6 +469,179 @@ public sealed class WalkFrameDriverTests
|
||||||
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
|
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── T2 (S3 chunk 2, R4): a fresh driver's first ov>0 interior flood
|
||||||
|
// skips the gated clear (PortalsDrawnCount starts at 0); that same
|
||||||
|
// frame's own exit seals arm the counter at Replay, so the driver's
|
||||||
|
// SECOND ov>0 flood — even with a different flood cell, since the
|
||||||
|
// counter is driver-scoped, not cell-scoped — sees it nonzero and
|
||||||
|
// clears. ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
var log = new List<string>();
|
||||||
|
var leaf = new RecordingLeafRenderer(log);
|
||||||
|
var ctx = new TestContext();
|
||||||
|
const uint cellId = 0xF4180200u;
|
||||||
|
var cell = new WalkCell { CellId = cellId };
|
||||||
|
cell.PushView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
cell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
ctx.Cells[cellId] = cell;
|
||||||
|
|
||||||
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
||||||
|
IWalkEventSink sink = driver;
|
||||||
|
|
||||||
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
|
||||||
|
// R4: PortalsDrawnCount starts at 0 for a fresh driver — the
|
||||||
|
// 0x005a489c-0x005a489e read-then-zero decision sees "not armed" on
|
||||||
|
// this driver's very first ov>0 interior-root flood, so the gated
|
||||||
|
// depth clear (pc:432731-432732) is skipped even though a landscape
|
||||||
|
// turn just ran.
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
|
||||||
|
var views1 = new WalkPortalView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
views1, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
sink.OnLandscapeViews(views1);
|
||||||
|
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"SKY", "TERRAIN:0", "LFLUSH", "SEALS",
|
||||||
|
"SHELL:f4180200", "CELL-PARTICLES:f4180200",
|
||||||
|
},
|
||||||
|
log);
|
||||||
|
Assert.DoesNotContain("CLEAR", log);
|
||||||
|
Assert.Equal(1, driver.PortalsDrawnCount);
|
||||||
|
log.Clear();
|
||||||
|
|
||||||
|
// Frame 1's exit seals reported SealPolygonsSubmitted (default 1) at
|
||||||
|
// Replay, arming PortalsDrawnCount for THIS frame's read-then-zero.
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
|
||||||
|
var views2 = new WalkPortalView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
views2, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
sink.OnLandscapeViews(views2);
|
||||||
|
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"SKY", "TERRAIN:0", "LFLUSH", "CLEAR", "SEALS",
|
||||||
|
"SHELL:f4180200", "CELL-PARTICLES:f4180200",
|
||||||
|
},
|
||||||
|
log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── T2 (S3 chunk 2, R4): a driver whose flood never reaches an exit
|
||||||
|
// portal (DrawExitSeals reports 0 submitted every turn) never clears,
|
||||||
|
// across any number of ov>0 frames — retail's counter is fed ONLY by
|
||||||
|
// actually-submitted seal fans. ─────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnInteriorFloodDrawTurn_FloodWithNoExitPortal_NeverClearsAcrossFrames()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
var log = new List<string>();
|
||||||
|
var leaf = new RecordingLeafRenderer(log) { SealPolygonsSubmitted = 0 };
|
||||||
|
var ctx = new TestContext();
|
||||||
|
const uint cellId = 0xF4180201u;
|
||||||
|
var cell = new WalkCell { CellId = cellId };
|
||||||
|
cell.PushView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
cell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
ctx.Cells[cellId] = cell;
|
||||||
|
|
||||||
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
||||||
|
IWalkEventSink sink = driver;
|
||||||
|
|
||||||
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
for (int frame = 0; frame < 3; frame++)
|
||||||
|
{
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
|
||||||
|
var views = new WalkPortalView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
views, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
sink.OnLandscapeViews(views);
|
||||||
|
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.DoesNotContain("CLEAR", log);
|
||||||
|
Assert.Equal(3, log.Count(entry => entry == "SEALS"));
|
||||||
|
Assert.Equal(0, driver.PortalsDrawnCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── T3 (S3 chunk 2, R1): a building look-in's own DrawCells re-enters
|
||||||
|
// with ov==0 unconditionally and neither ARMS nor CONSUMES the
|
||||||
|
// persistent PortalsDrawnCount counter — retail calls DrawCells
|
||||||
|
// re-entrantly there with no clear/seal step at all. ────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LookInDrawCells_NeitherArmsNorConsumesThePortalsDrawnCounter()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
var log = new List<string>();
|
||||||
|
var leaf = new RecordingLeafRenderer(log);
|
||||||
|
var ctx = new TestContext();
|
||||||
|
const uint rootCellId = 0xF4180301u;
|
||||||
|
const uint lookInCellId = 0xF4180302u;
|
||||||
|
var rootCell = new WalkCell { CellId = rootCellId };
|
||||||
|
rootCell.PushView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
rootCell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
ctx.Cells[rootCellId] = rootCell;
|
||||||
|
var lookInCell = new WalkCell { CellId = lookInCellId };
|
||||||
|
lookInCell.PushView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
lookInCell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
ctx.Cells[lookInCellId] = lookInCell;
|
||||||
|
|
||||||
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
||||||
|
IWalkEventSink sink = driver;
|
||||||
|
|
||||||
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
|
||||||
|
// Arm PortalsDrawnCount with one throwaway ov>0 interior-root flood.
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
|
||||||
|
var rootViews = new WalkPortalView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
rootViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
sink.OnLandscapeViews(rootViews);
|
||||||
|
sink.OnInteriorFloodDrawTurn([rootCellId], outsideViewCount: 1);
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
Assert.Equal(1, driver.PortalsDrawnCount);
|
||||||
|
log.Clear();
|
||||||
|
|
||||||
|
// A building look-in's own DrawCells re-enters with ov==0
|
||||||
|
// unconditionally (R1) — no LFLUSH/clear/seal step at all ("DrawCells
|
||||||
|
// re-entrantly there with no clear/seal step"), so it must leave the
|
||||||
|
// already-armed counter alone.
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
sink.OnBuildingTurn(new WalkBuilding());
|
||||||
|
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [lookInCellId]));
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("LFLUSH", log);
|
||||||
|
Assert.DoesNotContain("CLEAR", log);
|
||||||
|
Assert.DoesNotContain("SEALS", log);
|
||||||
|
Assert.Equal(1, driver.PortalsDrawnCount);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Deliverable: a building turn's alpha barrier precedes its portal
|
// ── Deliverable: a building turn's alpha barrier precedes its portal
|
||||||
// pass (retail RenderDeviceD3D::DrawBuilding @0x0059f2a0:
|
// pass (retail RenderDeviceD3D::DrawBuilding @0x0059f2a0:
|
||||||
// FlushAlphaList(0f) -> CPhysicsPart::Draw(parts,1) [the portal walk]
|
// FlushAlphaList(0f) -> CPhysicsPart::Draw(parts,1) [the portal walk]
|
||||||
|
|
@ -599,15 +807,22 @@ public sealed class WalkFrameDriverTests
|
||||||
|
|
||||||
using DrawScope draw = fx.BeginDraw();
|
using DrawScope draw = fx.BeginDraw();
|
||||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
// Neither call models a surviving exit view (ov==0 both times — no
|
||||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
// WalkEvent.Landscape turn ran in this synthetic double-entry
|
||||||
|
// scenario), so per S3 §8.1 R3 the device-stamp advance never fires
|
||||||
|
// from either call: this isolates the frame-stamp shell dedup from
|
||||||
|
// the landscape-flush/clear/seal gate entirely.
|
||||||
|
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 0);
|
||||||
|
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 0);
|
||||||
driver.EndFrame();
|
driver.EndFrame();
|
||||||
driver.Replay(draw.Frame, draw.Pass);
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
Assert.Equal([cellId], leaf.Shells);
|
Assert.Equal([cellId], leaf.Shells);
|
||||||
Assert.Equal(1, log.Count(entry => entry == "SHELL:f4180112"));
|
Assert.Equal(1, log.Count(entry => entry == "SHELL:f4180112"));
|
||||||
Assert.Equal(2, log.Count(entry => entry == "CLEAR"));
|
Assert.Equal(0, log.Count(entry => entry == "LFLUSH"));
|
||||||
Assert.Equal(2, log.Count(entry => entry == "SEALS"));
|
Assert.Equal(0, log.Count(entry => entry == "CLEAR"));
|
||||||
|
Assert.Equal(0, log.Count(entry => entry == "SEALS"));
|
||||||
|
Assert.Equal(0, driver.PortalsDrawnCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Campaign OVERHAUL S2 chunk 6 pin — the portal-haze bug this chunk
|
// Campaign OVERHAUL S2 chunk 6 pin — the portal-haze bug this chunk
|
||||||
|
|
@ -639,7 +854,8 @@ public sealed class WalkFrameDriverTests
|
||||||
|
|
||||||
using DrawScope draw = fx.BeginDraw();
|
using DrawScope draw = fx.BeginDraw();
|
||||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
sink.OnInteriorFloodDrawTurn([arrivalCellId]);
|
// No landscape turn modeled here — ov==0.
|
||||||
|
sink.OnInteriorFloodDrawTurn([arrivalCellId], outsideViewCount: 0);
|
||||||
driver.EndFrame();
|
driver.EndFrame();
|
||||||
driver.Replay(draw.Frame, draw.Pass);
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
|
|
@ -651,7 +867,6 @@ public sealed class WalkFrameDriverTests
|
||||||
{
|
{
|
||||||
using var fx = new DispatcherFixture();
|
using var fx = new DispatcherFixture();
|
||||||
var log = new List<string>();
|
var log = new List<string>();
|
||||||
var leaf = new RecordingLeafRenderer(log);
|
|
||||||
var ctx = new TestContext();
|
var ctx = new TestContext();
|
||||||
const uint cellId = 0xF4180112u;
|
const uint cellId = 0xF4180112u;
|
||||||
var cell = new WalkCell { CellId = cellId };
|
var cell = new WalkCell { CellId = cellId };
|
||||||
|
|
@ -666,11 +881,32 @@ public sealed class WalkFrameDriverTests
|
||||||
|
|
||||||
var driver = new WalkFrameDriver(
|
var driver = new WalkFrameDriver(
|
||||||
fx.Dispatcher,
|
fx.Dispatcher,
|
||||||
leaf,
|
new RecordingLeafRenderer(new List<string>()),
|
||||||
new FakeWorldData());
|
new FakeWorldData());
|
||||||
IWalkEventSink sink = driver;
|
IWalkEventSink sink = driver;
|
||||||
|
|
||||||
using DrawScope draw = fx.BeginDraw();
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
|
||||||
|
// R4 (the "first-frame no-clear quirk", pinned explicitly by
|
||||||
|
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClear...):
|
||||||
|
// PortalsDrawnCount starts at 0 for a fresh driver, so ITS first
|
||||||
|
// ov>0 flood would skip the clear. Prime the counter with one
|
||||||
|
// throwaway ov>0 flood (this driver's own exit seals arm it) so the
|
||||||
|
// documented scenario below models a STEADY-STATE interior frame,
|
||||||
|
// where the clear actually fires.
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
|
||||||
|
var primerViews = new WalkPortalView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
primerViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
sink.OnLandscapeViews(primerViews);
|
||||||
|
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
Assert.Equal(1, driver.PortalsDrawnCount);
|
||||||
|
|
||||||
|
var leaf = new RecordingLeafRenderer(log);
|
||||||
|
driver.RebindFrame(leaf, clipFrame: null);
|
||||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
|
||||||
// LScape::draw has begun. A building look-in reached this cell before
|
// LScape::draw has begun. A building look-in reached this cell before
|
||||||
|
|
@ -689,13 +925,16 @@ public sealed class WalkFrameDriverTests
|
||||||
|
|
||||||
// The same shell must draw again after the retail stamp increment and
|
// The same shell must draw again after the retail stamp increment and
|
||||||
// full depth clear; otherwise the pre-clear color survives unpaired
|
// full depth clear; otherwise the pre-clear color survives unpaired
|
||||||
// with depth and bleeds through the root's walls.
|
// with depth and bleeds through the root's walls. ov=1 (the
|
||||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
// landscape turn above ran), and PortalsDrawnCount is armed from
|
||||||
|
// the primer frame's own seals, so the clear actually fires here.
|
||||||
|
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
|
||||||
driver.EndFrame();
|
driver.EndFrame();
|
||||||
driver.Replay(draw.Frame, draw.Pass);
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
Assert.Equal([cellId, cellId], leaf.Shells);
|
Assert.Equal([cellId, cellId], leaf.Shells);
|
||||||
Assert.Equal(2, log.Count(entry => entry == "SHELL:f4180112"));
|
Assert.Equal(2, log.Count(entry => entry == "SHELL:f4180112"));
|
||||||
|
Assert.Contains("CLEAR", log);
|
||||||
Assert.True(
|
Assert.True(
|
||||||
log.IndexOf("SHELL:f4180112") < log.IndexOf("CLEAR"),
|
log.IndexOf("SHELL:f4180112") < log.IndexOf("CLEAR"),
|
||||||
"The look-in shell must precede the interior clear.");
|
"The look-in shell must precede the interior clear.");
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,46 @@ public sealed class WalkPViewFloodTests
|
||||||
Assert.Equal(1, far.TopView.ViewCount);
|
Assert.Equal(1, far.TopView.ViewCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── T4 / R2 (S3 chunk 2): PView::ConstructView @0x005a57b0 resets
|
||||||
|
// outside_view.view_count = 0, master_timestamp++, cell_todo_num = 0,
|
||||||
|
// and cell_draw_num = 0 BEFORE InitCell — a second flood on the SAME
|
||||||
|
// pview instance must not accumulate the first flood's draw list or
|
||||||
|
// outside view. ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstructView_CalledTwiceOnTheSameInstance_ResetsCellDrawListAndOutsideViewEachTime()
|
||||||
|
{
|
||||||
|
var ctx = new TestContext();
|
||||||
|
WalkCell first = Cell(ctx, 0x200,
|
||||||
|
(new WalkCellPortal { OtherCellId = 0xFFFFFFFF, PolygonIndex = 0, PortalSide = 0, OtherPortalId = -1 },
|
||||||
|
Quad(-2f)));
|
||||||
|
WalkCell second = Cell(ctx, 0x300,
|
||||||
|
(new WalkCellPortal { OtherCellId = 0x301, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
|
||||||
|
Quad(-2f)));
|
||||||
|
Cell(ctx, 0x301,
|
||||||
|
(new WalkCellPortal { OtherCellId = 0x300, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0 },
|
||||||
|
Quad(-2f)));
|
||||||
|
|
||||||
|
var pview = new WalkPView();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
first.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
pview.ConstructView(first, 0xFFFF, ctx);
|
||||||
|
Assert.Equal(new[] { 0x200u }, pview.CellDrawList.Select(c => c.CellId));
|
||||||
|
Assert.Equal(1, pview.OutsideView.ViewCount);
|
||||||
|
int timestampAfterFirst = WalkPView.MasterTimestampForDiagnostics;
|
||||||
|
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
second.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
pview.ConstructView(second, 0xFFFF, ctx);
|
||||||
|
|
||||||
|
// The second flood's own cells only — NOT the first flood's 0x200
|
||||||
|
// still sitting in CellDrawList, and the exit-facing portal from the
|
||||||
|
// first flood does not leave OutsideView.ViewCount stuck at 1.
|
||||||
|
Assert.Equal(new[] { 0x300u, 0x301u }, pview.CellDrawList.Select(c => c.CellId));
|
||||||
|
Assert.Equal(0, pview.OutsideView.ViewCount);
|
||||||
|
Assert.True(WalkPView.MasterTimestampForDiagnostics > timestampAfterFirst);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Unloaded_neighbor_is_silently_skipped()
|
public void Unloaded_neighbor_is_silently_skipped()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue