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
|
|
@ -180,6 +180,27 @@ public sealed class RetailFrameWalkTests
|
|||
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]
|
||||
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<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 DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
|
||||
|
|
@ -58,9 +68,15 @@ public sealed class WalkFrameDriverTests
|
|||
log.Add($"SHELL:{cellId:x8}");
|
||||
}
|
||||
|
||||
public void FlushLandscape() => log.Add("LFLUSH");
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -189,19 +205,24 @@ public sealed class WalkFrameDriverTests
|
|||
// PView::DrawCells' exact two reverse loops: ALL shells far-to-near,
|
||||
// then ALL object cells far-to-near. ─────────────────────────────────
|
||||
|
||||
// ── Deliverable (2026-08-30 decomp correction): PView::DrawCells
|
||||
// @0x005a4840's actual DRAW order for an interior root's OWN flood is
|
||||
// NOT the order its DrawInside/DrawCells EVENTS fire in (breakpoint-
|
||||
// entry order, matching the FW0 oracle traces) — retail draws
|
||||
// LScape::draw FIRST (pc:432719, only when exit views survived), then
|
||||
// the depth clear (pc:432731-432732), then the exit-portal seals
|
||||
// (pc:432785-432786), and ONLY THEN the flood's 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 (flush no-op, sky, terrain), THEN
|
||||
// clear, seals, then all shells and all contents in reverse order. ───
|
||||
// ── Deliverable (S3 chunk 2, superseding the 2026-08-30 decomp
|
||||
// correction): PView::DrawCells @0x005a4840's actual DRAW order for an
|
||||
// interior root's OWN flood is NOT the order its DrawInside/DrawCells
|
||||
// EVENTS fire in (breakpoint-entry order, matching the FW0 oracle
|
||||
// traces) — retail draws LScape::draw FIRST (pc:432719, only when exit
|
||||
// views survived), then the landscape flush + device-stamp advance,
|
||||
// then a GATED depth clear (pc:432731-432732 — R4: read-then-zero
|
||||
// portalsDrawnCount @0x005a489c-0x005a489e, clear iff nonzero), then
|
||||
// the exit-portal seals (pc:432785-432786), and ONLY THEN the flood's
|
||||
// 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]
|
||||
public void RunFrame_InteriorFloodWithExitView_DrawsLandscapeThenClearSealsThenFloodCells()
|
||||
public void RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
|
|
@ -271,12 +292,19 @@ public sealed class WalkFrameDriverTests
|
|||
Assert.Equal(
|
||||
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",
|
||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
|
||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
|
||||
},
|
||||
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 =
|
||||
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||
|
|
@ -346,7 +374,9 @@ public sealed class WalkFrameDriverTests
|
|||
ctx.ViewportHeight);
|
||||
sink.OnLandscapeViews(landscapeViews);
|
||||
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.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
|
|
@ -359,13 +389,15 @@ public sealed class WalkFrameDriverTests
|
|||
|| entry == "FLUSH:1:CellStatic"));
|
||||
}
|
||||
|
||||
// ── Deliverable: the ov==0 interior case — no exit view survives, so
|
||||
// DrawInside never runs the landscape turn at all; retail's clear+seals
|
||||
// still run unconditionally for the interior root's own flood, straight
|
||||
// after the (draw-nothing) DC event. ───────────────────────────────────
|
||||
// ── Deliverable (T1, flipped for S3 chunk 2): the ov==0 interior case —
|
||||
// no exit view survives, so DrawInside never runs the landscape turn,
|
||||
// and PView::DrawCells' whole landscape-flush/stamp/clear/seal turn
|
||||
// (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]
|
||||
public void RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeButStillClearsAndSeals()
|
||||
public void RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeAndNeverFlushesClearsOrSeals()
|
||||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
|
|
@ -416,15 +448,18 @@ public sealed class WalkFrameDriverTests
|
|||
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
|
||||
|
||||
// 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(
|
||||
new[]
|
||||
{
|
||||
"CLEAR", "SEALS", "SHELL:00000101", "SHELL:00000100",
|
||||
"SHELL:00000101", "SHELL:00000100",
|
||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000101",
|
||||
"FLUSH:1:CellStatic", "CELL-PARTICLES:00000100",
|
||||
},
|
||||
log);
|
||||
Assert.Equal(0, driver.PortalsDrawnCount);
|
||||
|
||||
List<GpuRecordedMultiDrawIndirect> mdiCalls =
|
||||
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
|
||||
|
|
@ -434,6 +469,179 @@ public sealed class WalkFrameDriverTests
|
|||
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
|
||||
// pass (retail RenderDeviceD3D::DrawBuilding @0x0059f2a0:
|
||||
// FlushAlphaList(0f) -> CPhysicsPart::Draw(parts,1) [the portal walk]
|
||||
|
|
@ -599,15 +807,22 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
||||
// Neither call models a surviving exit view (ov==0 both times — no
|
||||
// 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.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal([cellId], leaf.Shells);
|
||||
Assert.Equal(1, log.Count(entry => entry == "SHELL:f4180112"));
|
||||
Assert.Equal(2, log.Count(entry => entry == "CLEAR"));
|
||||
Assert.Equal(2, log.Count(entry => entry == "SEALS"));
|
||||
Assert.Equal(0, log.Count(entry => entry == "LFLUSH"));
|
||||
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
|
||||
|
|
@ -639,7 +854,8 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
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.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
|
|
@ -651,7 +867,6 @@ public sealed class WalkFrameDriverTests
|
|||
{
|
||||
using var fx = new DispatcherFixture();
|
||||
var log = new List<string>();
|
||||
var leaf = new RecordingLeafRenderer(log);
|
||||
var ctx = new TestContext();
|
||||
const uint cellId = 0xF4180112u;
|
||||
var cell = new WalkCell { CellId = cellId };
|
||||
|
|
@ -666,11 +881,32 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
var driver = new WalkFrameDriver(
|
||||
fx.Dispatcher,
|
||||
leaf,
|
||||
new RecordingLeafRenderer(new List<string>()),
|
||||
new FakeWorldData());
|
||||
IWalkEventSink sink = driver;
|
||||
|
||||
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);
|
||||
|
||||
// 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
|
||||
// full depth clear; otherwise the pre-clear color survives unpaired
|
||||
// with depth and bleeds through the root's walls.
|
||||
sink.OnInteriorFloodDrawTurn([cellId]);
|
||||
// with depth and bleeds through the root's walls. ov=1 (the
|
||||
// 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.Replay(draw.Frame, draw.Pass);
|
||||
|
||||
Assert.Equal([cellId, cellId], leaf.Shells);
|
||||
Assert.Equal(2, log.Count(entry => entry == "SHELL:f4180112"));
|
||||
Assert.Contains("CLEAR", log);
|
||||
Assert.True(
|
||||
log.IndexOf("SHELL:f4180112") < log.IndexOf("CLEAR"),
|
||||
"The look-in shell must precede the interior clear.");
|
||||
|
|
|
|||
|
|
@ -177,6 +177,46 @@ public sealed class WalkPViewFloodTests
|
|||
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]
|
||||
public void Unloaded_neighbor_is_silently_skipped()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue