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

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

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

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

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

View file

@ -49,6 +49,10 @@ public sealed class WalkFrameDriverTests
public void DrawCellShell(uint cellId) => log.Add($"SHELL:{cellId:x8}");
public void ClearInteriorDepth() => log.Add("CLEAR");
public void DrawExitSeals() => log.Add("SEALS");
public void DrawPunchFan(WalkPolygon worldPolygon)
{
Punches.Add(worldPolygon);
@ -154,8 +158,19 @@ public sealed class WalkFrameDriverTests
// the NEXT cell's shell needs the stream clear (never before, never
// batched across cells within this stage's turn-by-turn discipline). ──
// ── 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, and the two flood cells shell-then-contents. ──────────
[Fact]
public void RunFrame_InteriorTwoCellFlood_EmitsShellThenContentsPerCellWithAFlushBetween()
public void RunFrame_InteriorFloodWithExitView_DrawsLandscapeThenClearSealsThenFloodCells()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
@ -166,6 +181,94 @@ public sealed class WalkFrameDriverTests
InjectRenderData(fx.Manager, gfxObjB, MakeFlatMesh(
MakeBatch(0x08100002u, TranslucencyKind.Opaque, 3, 4, 3, 2)));
var ctx = new TestContext();
var cell1 = new WalkCell
{
CellId = 0x100,
StabList = [0x101u],
Portals =
[
new WalkCellPortal
{
OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0,
},
// The exit portal (retail's "world beyond the door") — this
// is what raises ov to 1 and makes DrawInside run the
// landscape turn before clear+seals+the flood cells.
new WalkCellPortal
{
OtherCellId = 0xFFFFFFFF, PolygonIndex = 1, PortalSide = 0, OtherPortalId = -1,
},
],
PortalPolygons = [Quad(-2f), Quad(-3f)],
};
var cell2 = new WalkCell
{
CellId = 0x101,
Portals = [new WalkCellPortal
{
OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[cell1.CellId] = cell1;
ctx.Cells[cell2.CellId] = cell2;
var worldData = new FakeWorldData();
worldData.CellStaticsByCell[0x100] = new WalkFrameStaticRecords(
[MakeRecord(101, 0, Vector3.Zero, [new MeshRef((uint)gfxObjA, Matrix4x4.Identity)])], 0x8C04u);
worldData.CellStaticsByCell[0x101] = new WalkFrameStaticRecords(
[MakeRecord(102, 0, Vector3.Zero, [new MeshRef((uint)gfxObjB, Matrix4x4.Identity)])], 0x8C04u);
var leaf = new RecordingLeafRenderer(log);
var trace = new RecordingTrace(log);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, trace);
var walk = new RetailFrameWalk();
// A minimal, no-op landscape (1x1 window, the one slot unpublished)
// — matches RetailFrameWalkTests' own exit-view fixture. LScape::draw
// still runs its full turn (sky/terrain) against it; there's simply
// nothing published to iterate blocks/cells/buildings for.
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
using DrawScope draw = fx.BeginDraw();
driver.RunFrame(
walk, cameraCellId: cell1.CellId, cameraCell: cell1, landscape: landscape,
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero,
activeTerrainSliceCount: 1);
Assert.Equal(
new[]
{
"SKY", "TERRAIN:0", "CLEAR", "SEALS",
"SHELL:00000100", "FLUSH:1:CellStatic", "SHELL:00000101", "FLUSH:1:CellStatic",
},
log);
List<GpuRecordedMultiDrawIndirect> mdiCalls =
[.. fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()];
Assert.Equal(2, mdiCalls.Count);
Assert.All(mdiCalls, c => Assert.Equal(1u, c.DrawCount));
// Nothing dropped: every populated record reached exactly one indirect draw.
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
}
// ── 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. ───────────────────────────────────
[Fact]
public void RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeButStillClearsAndSeals()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
const ulong gfxObjA = 0x0200_0003UL;
const ulong gfxObjB = 0x0200_0004UL;
InjectRenderData(fx.Manager, gfxObjA, MakeFlatMesh(
MakeBatch(0x08100003u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
InjectRenderData(fx.Manager, gfxObjB, MakeFlatMesh(
MakeBatch(0x08100004u, TranslucencyKind.Opaque, 3, 4, 3, 2)));
var ctx = new TestContext();
var cell1 = new WalkCell
{
@ -206,8 +309,10 @@ public sealed class WalkFrameDriverTests
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero,
activeTerrainSliceCount: 0);
// No SKY/TERRAIN — ov==0 means DrawInside never calls DrawLandscape
// at all — but CLEAR/SEALS still fire unconditionally.
Assert.Equal(
new[] { "SHELL:00000100", "FLUSH:1:CellStatic", "SHELL:00000101", "FLUSH:1:CellStatic" },
new[] { "CLEAR", "SEALS", "SHELL:00000100", "FLUSH:1:CellStatic", "SHELL:00000101", "FLUSH:1:CellStatic" },
log);
List<GpuRecordedMultiDrawIndirect> mdiCalls =