feat(render) Campaign FW3.2b-1: the walk frame driver

WalkFrameDriver executes one full static-content frame from the walk's
turns so GPU command-buffer order equals retail's walk order. One rule
does the interleaving: the accumulated OrderedDrawStream flushes
through SubmitOrderedStream immediately before EVERY non-stream draw
(sky, terrain slice, cell shell, punch fan, alpha barrier).

Turn script, all decomp-cited and two of them corrected in review:
- Interior flood: per cell IN FLOOD ORDER, shell first then contents
  (PView::DrawCells @0x005a4840: DrawEnvCell @0x005a4abe precedes
  DrawObjCellForDummies @0x005a4b0d).
- Landscape: sky once, terrain per active slice, then blocks
  far-to-near; per cell the building turn precedes the cell's outdoor
  statics (DrawSortCell @0x0059f140).
- Building (DrawBuilding @0x0059f2a0): the BLD probe event stays at
  entry, but the ENTIRE body - alpha barrier, portal passes, shell -
  sits inside retail's gfxobj[deg_level]!=0 gate @0x0059f2d3, and the
  order is FlushAlphaList @0x0059f30b -> the two-pass punch/look-in
  walk -> THEN the shell draw @0x0059f345. The driver review caught
  both the missing gate and a shell-before-punch inversion; fixed
  with the addresses cited.

Walk seam: three additive default-implemented IWalkEventSink hooks
(OnLandscapeCellTurn / OnBuildingTurn / OnBuildingShellTurn /
OnPunchGeometry) - every existing sink and all FW1 conformance
fixtures unchanged. WalkLandBlock gains LandblockId for the cell-id
encoding. Leaf draws go through IWalkFrameLeafRenderer so FW3.2b-2
wires the real renderers and the referee suite runs on fakes +
RecordingGpuDevice.

Flagged for FW3.2b-2/FW4 adjudication (documented in code):
FlushFartherThan(building distance) vs retail flush-all
FlushAlphaList(0f); terrain-before-statics within the landscape turn.

Suites: full Release build 0 warnings; Walk lane 200/1 skip;
InstalledDat Walk conformance 40/1 untouched; hermetic 6,752/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 14:23:17 +02:00
parent 81c6531727
commit 03f63686cc
6 changed files with 1303 additions and 7 deletions

View file

@ -142,17 +142,33 @@ public sealed class RetailFrameWalk
{
continue;
}
// RenderDeviceD3D::DrawSortCell @0x0059f140 (decomp-
// confirmed 2026-08-30): DrawBuilding(building) FIRST, then
// DrawObjCell(cell) UNCONDITIONALLY — the building's turn
// (shell + portal machinery) precedes this cell's own
// outdoor-static turn.
if (block.CellBuildings[cellIndex] is WalkBuilding building)
DrawBuilding(building, activeViews, ctx, sink);
sink.OnLandscapeCellTurn((block.LandblockId & 0xFFFF0000u) | (uint)(cellIndex + 1));
}
}
}
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0 at the walk
/// level: the BLD event fires at ENTRY (before the degrade check); the
/// portal pass walks the drawing BSP twice per active view (punch, then
/// look-in + DrawCells on the outdoor pview); the shell pass emits no
/// walk event.</summary>
/// level: the BLD event fires at ENTRY (before the degrade check — the
/// oracle traces' breakpoint sat there, so this stays unconditional for
/// conformance). The ENTIRE rest of the body — the alpha barrier, the
/// portal pass, and the shell draw — sits inside retail's
/// <c>if (part-&gt;gfxobj[part-&gt;deg_level] != 0)</c> @0x0059f2d3; a
/// degraded-out slot draws NOTHING beyond the BLD event.
/// <c>HasGeometry</c> + a non-null <c>SelectDrawingBsp</c> together model
/// that one gate. Inside the gate, retail's own order
/// (@0x0059f30b0x0059f345) is <c>D3DPolyRender::FlushAlphaList(0f)</c> →
/// <c>CPhysicsPart::Draw(parts, 1)</c> (the PORTAL flavor — the two-pass
/// punch/look-in walk below) → <c>CPhysicsPart::Draw(parts, 0)</c> (the
/// plain mesh — the building's own SHELL) → flag reset: the alpha
/// barrier and the portal pass both precede the shell draw, not follow
/// it.</summary>
public void DrawBuilding(
WalkBuilding building, WalkPortalView activeViews,
IRetailFrameWalkContext ctx, IWalkEventSink sink)
@ -167,8 +183,15 @@ public sealed class RetailFrameWalk
degradeMultiplier: DegradeMultiplier);
if (bsp is null) return;
// Additive (Campaign FW3.2b-1): the alpha barrier
// (D3DPolyRender::FlushAlphaList(0f) @0x0059f30b) — gated by the
// SAME part->gfxobj[deg_level]!=0 check as everything below it, so
// this fires only now that both HasGeometry and the bsp lookup have
// passed.
sink.OnBuildingTurn(building);
int viewCount = Math.Max(activeViews.ViewCount, 0);
var passSink = new PortalPassSink(sink);
var passSink = new PortalPassSink(building, sink);
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
for (int v = 0; v < viewCount; v++)
{
@ -182,6 +205,12 @@ public sealed class RetailFrameWalk
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
_outdoorPView, building, portalRef, pass, ctx, passSink));
}
// Additive (Campaign FW3.2b-1): CPhysicsPart::Draw(parts, 0)
// @0x0059f331 — the building's own shell mesh — runs AFTER the
// portal walk completes (CPhysicsPart::Draw(parts, 1) just above),
// not before it.
sink.OnBuildingShellTurn(building);
}
private void EmitDrawCells(WalkPView pview, IWalkEventSink sink)
@ -204,13 +233,17 @@ public sealed class RetailFrameWalk
ctx.GetVisible(id)?.PopView();
}
private sealed class PortalPassSink(IWalkEventSink sink)
private sealed class PortalPassSink(WalkBuilding building, IWalkEventSink sink)
: WalkBuildingPortals.IWalkPortalPassSink
{
public void OnPunch(WalkPolygon polygon)
{
// The far-Z punch is a depth-only GPU submission (FW2's ordered
// stream); the oracle traces do not log it.
// stream); the oracle traces do not log it. Additive (Campaign
// FW3.2b-1): forward to the richer sink so a driver can flush +
// draw the punch fan — building-local space, world transform is
// the driver's job (WalkProductionFrameContext-style lookup).
sink.OnPunchGeometry(building, polygon);
}
public void OnDrawCells(WalkPView pview)