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:
parent
03f63686cc
commit
035d7b0148
4 changed files with 247 additions and 20 deletions
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue