acdream/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs
Erik 03f63686cc 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>
2026-08-30 14:23:17 +02:00

257 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 &lt; 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&gt;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-&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)
{
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));
}
}
}