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>
257 lines
12 KiB
C#
257 lines
12 KiB
C#
using System.Numerics;
|
||
|
||
namespace AcDream.App.Rendering.Walk;
|
||
|
||
/// <summary>The composition context: the cell/building halves plus the
|
||
/// frame-level state retail keeps in globals (the CY near plane from
|
||
/// <c>Render::update_viewpoint</c>, and the active-view installation the
|
||
/// building clip consumes).</summary>
|
||
public interface IRetailFrameWalkContext : IWalkBuildingFrameContext
|
||
{
|
||
WalkPlane CyPlane { get; }
|
||
|
||
/// <summary><c>Render::set_view</c> at the frame level: install view
|
||
/// <paramref name="index"/> of <paramref name="views"/> as the active
|
||
/// clip context for subsequent building-polygon clips.</summary>
|
||
void SetActiveView(WalkPortalView views, int index);
|
||
|
||
float ViewportWidth { get; }
|
||
float ViewportHeight { get; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Campaign FW1 — the frame walk root, composing the ported machinery into
|
||
/// retail's frame shapes (model:
|
||
/// docs/research/2026-08-30-fw-walk-pseudocode.md §1, §3-§5; oracle:
|
||
/// docs/research/2026-08-30-fw-walk-oracle/README.md):
|
||
///
|
||
/// <list type="bullet">
|
||
/// <item>Rooting (<c>SmartBox::RenderNormalMode</c> @0x00453aa0): the
|
||
/// CAMERA cell's low word < 0x100 → outdoor root (default full-screen
|
||
/// view + the landscape walk); otherwise → <c>DrawInside</c>.</item>
|
||
/// <item>Outdoor (<c>LScape::draw</c> @0x00506330): visibility per view,
|
||
/// blocks far-to-near, per-block cells far-to-near, buildings at their
|
||
/// cell's turn with the two-pass portal machinery (punches + look-ins on
|
||
/// the OUTDOOR pview — the traces' <c>pv=009d4b08 ov=0</c>).</item>
|
||
/// <item>Interior (<c>PView::DrawInside</c> @0x005a5860 +
|
||
/// <c>DrawCells</c> @0x005a4840): the flood on the INTERIOR pview
|
||
/// (<c>pv=009d4a80</c>), then the landscape drawn THROUGH the exit views
|
||
/// when any survive (<c>ov>0</c>).</item>
|
||
/// </list>
|
||
///
|
||
/// Two PView instances exist exactly as the live traces showed.
|
||
/// </summary>
|
||
public sealed class RetailFrameWalk
|
||
{
|
||
private readonly WalkPView _interiorPView = new();
|
||
// The outdoor pview never draws the landscape through its look-in
|
||
// floods (the landscape is already drawn when buildings punch), so its
|
||
// draw_landscape is FALSE and look-in DCs always read ov=0 — exactly
|
||
// the pv=…/ov=0 pattern of every look-in in the oracle traces.
|
||
private readonly WalkPView _outdoorPView = new() { DrawLandscape = false };
|
||
private readonly WalkPortalView _defaultView = new();
|
||
|
||
public WalkPView InteriorPView => _interiorPView;
|
||
public WalkPView OutdoorPView => _outdoorPView;
|
||
|
||
/// <summary>Retail global <c>alwaysDrawObjects</c> (.data default 1
|
||
/// @0x00820ed4): cell contents draw for every cell of an in-view block
|
||
/// regardless of per-cell visibility.</summary>
|
||
public bool AlwaysDrawObjects = true;
|
||
|
||
/// <summary>Retail global <c>Render::deg_mul</c> — DYNAMIC: the
|
||
/// auto-tuner (<c>auto_update_deg_mul</c>) swings it with frame load
|
||
/// (positive ⇒ degrade thresholds slide toward each level's max;
|
||
/// negative ⇒ toward its min). The oracle captures pin ≈+0.99 for every
|
||
/// fixture except doorway-still, whose capture ran under cdb load with
|
||
/// the multiplier depressed (the recon session's live dump read −0.99
|
||
/// under the same conditions).</summary>
|
||
public float DegradeMultiplier = WalkBuilding.DefaultDegradeMultiplier;
|
||
|
||
/// <summary>The per-frame root (<c>SmartBox::RenderNormalMode</c>).
|
||
/// <paramref name="cameraCell"/> may be null only when the camera is
|
||
/// outdoors.</summary>
|
||
public void WalkFrame(
|
||
uint cameraCellId, WalkCell? cameraCell, WalkLandscape landscape,
|
||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||
{
|
||
if ((cameraCellId & 0xFFFF) < 0x100)
|
||
{
|
||
// Render::set_default_view @0x0054ef50: the full-screen quad view.
|
||
_defaultView.ResetForPush();
|
||
WalkCopyView.AppendFullViewportQuad(
|
||
_defaultView, ctx.Rays, ctx.WorldViewpoint,
|
||
ctx.ViewportWidth, ctx.ViewportHeight);
|
||
DrawLandscape(landscape, _defaultView, ctx, sink);
|
||
}
|
||
else
|
||
{
|
||
DrawInside(
|
||
cameraCell ?? throw new ArgumentNullException(nameof(cameraCell)),
|
||
landscape, ctx, sink);
|
||
}
|
||
}
|
||
|
||
/// <summary><c>PView::DrawInside</c> + the event half of
|
||
/// <c>DrawCells</c>. The geometry/object passes emit no walk events.</summary>
|
||
public void DrawInside(
|
||
WalkCell cell, WalkLandscape landscape,
|
||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||
{
|
||
sink.Emit(WalkEvent.DrawInside(cell.CellId));
|
||
cell.PushView();
|
||
AddViews(cell.StabList, ctx);
|
||
WalkCopyView.AppendFullViewportQuad(
|
||
cell.TopView, ctx.Rays, ctx.WorldViewpoint,
|
||
ctx.ViewportWidth, ctx.ViewportHeight);
|
||
_interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
|
||
|
||
EmitDrawCells(_interiorPView, sink);
|
||
if (_interiorPView.OutsideView.ViewCount > 0)
|
||
DrawLandscape(landscape, _interiorPView.OutsideView, ctx, sink);
|
||
|
||
RemoveViews(cell.StabList, ctx);
|
||
cell.PopView();
|
||
}
|
||
|
||
/// <summary><c>LScape::draw</c>: visibility per active view, then blocks
|
||
/// far-to-near, cells far-to-near, buildings at their cell's turn.</summary>
|
||
public void DrawLandscape(
|
||
WalkLandscape landscape, WalkPortalView activeViews,
|
||
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||
{
|
||
sink.Emit(WalkEvent.Landscape());
|
||
landscape.CalcDrawOrder();
|
||
landscape.CheckBlocks(ctx.CyPlane, activeViews);
|
||
|
||
for (int i = landscape.BlockDrawCount - 1; i >= 0; i--)
|
||
{
|
||
WalkLandBlock? block = landscape.Blocks[landscape.BlockDrawList[i]];
|
||
if (block is null || block.InView == WalkBoundingType.Outside)
|
||
continue;
|
||
int cellCount = block.SideCellCount * block.SideCellCount;
|
||
for (int k = 0; k < cellCount; k++)
|
||
{
|
||
int cellIndex = block.DrawArray[k];
|
||
// RenderDeviceD3D::DrawBlock @0x005a19d9: DrawSortCell runs
|
||
// when alwaysDrawObjects != 0 (retail .data default 1
|
||
// @0x00820ed4) OR the cell IsInView; terrain (DrawLandCell)
|
||
// emits no walk event.
|
||
if (!AlwaysDrawObjects
|
||
&& block.CellInView[cellIndex] == WalkBoundingType.Outside)
|
||
{
|
||
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
|
||
/// 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->gfxobj[part->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
|
||
/// (@0x0059f30b–0x0059f345) 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)
|
||
{
|
||
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
||
if (!building.HasGeometry) return;
|
||
// Retail walks the CURRENT degrade level's drawing BSP
|
||
// (part->gfxobj[deg_level]); a degraded-out slot skips everything
|
||
// after publishing the portal list.
|
||
WalkBspNode? bsp = building.SelectDrawingBsp(
|
||
ctx.ViewerDistanceTo(building),
|
||
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(building, sink);
|
||
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
|
||
for (int v = 0; v < viewCount; v++)
|
||
{
|
||
ctx.SetActiveView(activeViews, v);
|
||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||
bsp, 1, viewpoint,
|
||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||
bsp, 2, viewpoint,
|
||
(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)
|
||
{
|
||
uint[] cells = new uint[pview.CellDrawList.Count];
|
||
for (int i = 0; i < cells.Length; i++)
|
||
cells[i] = pview.CellDrawList[i].CellId;
|
||
sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
|
||
}
|
||
|
||
private void AddViews(uint[] stabList, IRetailFrameWalkContext ctx)
|
||
{
|
||
foreach (uint id in stabList)
|
||
ctx.GetVisible(id)?.PushView();
|
||
}
|
||
|
||
private void RemoveViews(uint[] stabList, IRetailFrameWalkContext ctx)
|
||
{
|
||
foreach (uint id in stabList)
|
||
ctx.GetVisible(id)?.PopView();
|
||
}
|
||
|
||
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. 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)
|
||
{
|
||
uint[] cells = new uint[pview.CellDrawList.Count];
|
||
for (int i = 0; i < cells.Length; i++)
|
||
cells[i] = pview.CellDrawList[i].CellId;
|
||
sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
|
||
}
|
||
}
|
||
}
|