Additive, nothing invokes it yet (the cutover flip is step 2): - WalkProductionLeafRenderer + RetailPViewPassExecutor.WalkLeaf: the walk driver's leaf turns over the SAME executor renderers the packed path uses today - DrawWalkSky (the per-slice sky block looped under one driver turn), DrawWalkTerrainSlice (the terrain block of DrawLandscapeSlice), per-cell shells via EnvCellRenderer, DrawWalkPunchFan (PortalDepthMaskRenderer far-Z, +ShellDrawLiftZ matching today's DrawPortalDepthWrite until FW3.3 retires it), the alpha barrier via FlushLandscapeAlphaFartherThan, and caller-supplied clear/seal actions (the renderer owns the pass scope). The cutover changes ORDER, never leaf mechanics. - Punch fans now carry the ACTIVE VIEW INDEX end to end (retail pins building_view = Render::portal_view_num @0x0059f3bf for the whole two-pass walk; the fan clips by that view's slice planes): PortalPassSink.ActiveViewIndex -> IWalkEventSink.OnPunchGeometry -> IWalkFrameLeafRenderer.DrawPunchFan. - The FW3.2b-2 rooting design is recorded in the plan (dual-compute split, LookInObject route filtered to dynamics, consumer re-pointing, gate list). Suites: full Release build 0 warnings; Walk lane green; hermetic green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
161 lines
8.2 KiB
C#
161 lines
8.2 KiB
C#
namespace AcDream.App.Rendering.Walk;
|
||
|
||
/// <summary>
|
||
/// Campaign FW1 — the frame walk's observable event stream. These events
|
||
/// mirror the FW0 oracle-trace vocabulary one-to-one
|
||
/// (docs/research/2026-08-30-fw-walk-oracle/README.md): the conformance
|
||
/// contract is that <c>RetailFrameWalk</c>, fed the same world state and
|
||
/// camera pose as a capture, emits the identical sequence. Richer draw
|
||
/// commands (mesh/state/stage tuples for the FW2 ordered stream) layer on
|
||
/// later; this vocabulary stays the conformance surface.
|
||
/// </summary>
|
||
public enum WalkEventKind
|
||
{
|
||
/// <summary><c>LScape::draw</c> @0x00506330 ran (outdoor root, or drawn
|
||
/// through an interior frame's exit views).</summary>
|
||
Landscape,
|
||
|
||
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0 entered for
|
||
/// one building (identified by its position cell id).</summary>
|
||
Building,
|
||
|
||
/// <summary><c>PView::DrawInside</c> @0x005a5860 entered — the frame is
|
||
/// rooted at this interior cell.</summary>
|
||
DrawInside,
|
||
|
||
/// <summary><c>PView::DrawCells</c> @0x005a4840 entered with the flood's
|
||
/// cell list (far-to-near iteration happens inside; the event carries the
|
||
/// list in storage order, matching the oracle dump).</summary>
|
||
DrawCells,
|
||
}
|
||
|
||
public readonly record struct WalkEvent(
|
||
WalkEventKind Kind,
|
||
uint CellId,
|
||
int OutsideViewCount,
|
||
IReadOnlyList<uint> Cells)
|
||
{
|
||
public static WalkEvent Landscape()
|
||
=> new(WalkEventKind.Landscape, 0, 0, Array.Empty<uint>());
|
||
|
||
public static WalkEvent Building(uint positionCellId)
|
||
=> new(WalkEventKind.Building, positionCellId, 0, Array.Empty<uint>());
|
||
|
||
public static WalkEvent DrawInside(uint cellId)
|
||
=> new(WalkEventKind.DrawInside, cellId, 0, Array.Empty<uint>());
|
||
|
||
public static WalkEvent DrawCells(int outsideViewCount, IReadOnlyList<uint> cells)
|
||
=> new(WalkEventKind.DrawCells, 0, outsideViewCount, cells);
|
||
}
|
||
|
||
/// <summary>
|
||
/// The sink a walk emits into. FW1 conformance tests collect events; FW2's
|
||
/// production sink will additionally receive the ordered draw stream.
|
||
///
|
||
/// <para>Campaign FW3.2b-1 additive seam: <see cref="RetailFrameWalk"/> also
|
||
/// calls the five richer, default-no-op members below at turns the
|
||
/// vocabulary-only <see cref="WalkEvent"/> stream cannot express (a visited
|
||
/// landscape cell with no building emits no <see cref="WalkEvent"/> at all;
|
||
/// <see cref="WalkEventKind.Building"/> carries only a cell id, not the
|
||
/// <see cref="WalkBuilding"/> reference a driver needs to look up shell
|
||
/// content, degrade state, or world transform; and punches are not part of
|
||
/// the FW1 conformance vocabulary at all — the oracle traces do not log
|
||
/// them). Every FW1/FW2 sink that only implements <see cref="Emit"/>
|
||
/// continues to compile and behave identically — these are C# default
|
||
/// interface members, never called by any pre-FW3.2b-1 code path.</para>
|
||
/// </summary>
|
||
public interface IWalkEventSink
|
||
{
|
||
void Emit(in WalkEvent walkEvent);
|
||
|
||
/// <summary>
|
||
/// Fires once per visited landscape cell, AFTER that cell's building
|
||
/// turn (if any) — <c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140
|
||
/// calls <c>DrawBuilding(building)</c> first, then
|
||
/// <c>DrawObjCell(cell)</c> UNCONDITIONALLY (decomp-confirmed
|
||
/// 2026-08-30). <paramref name="cellId"/> follows retail's outdoor cell
|
||
/// encoding — <c>(landblockId & 0xFFFF0000) | (cellIndex + 1)</c> —
|
||
/// the same convention <see cref="WalkLandscapeAssembler.BuildSlot"/>
|
||
/// and <see cref="WalkProductionFrameContext.SetViewer"/> +
|
||
/// <see cref="WalkLandscapeAssembler.SetViewer"/> already use. Default
|
||
/// no-op.
|
||
/// </summary>
|
||
void OnLandscapeCellTurn(uint cellId) { }
|
||
|
||
/// <summary>
|
||
/// Fires at <see cref="RetailFrameWalk.DrawBuilding"/> once retail's own
|
||
/// gate has passed — <c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0
|
||
/// wraps its ENTIRE body (the alpha barrier, the portal pass, the shell
|
||
/// draw) in <c>if (part->gfxobj[part->deg_level] != 0)</c>
|
||
/// @0x0059f2d3 — a degraded-out slot draws NOTHING beyond the
|
||
/// unconditional <see cref="WalkEventKind.Building"/> <see cref="Emit"/>
|
||
/// call. <see cref="WalkBuilding.HasGeometry"/> and a non-null
|
||
/// <see cref="WalkBuilding.SelectDrawingBsp"/> result together model that
|
||
/// one gate, so this hook fires only after both have passed. This is the
|
||
/// ALPHA BARRIER turn (<c>D3DPolyRender::FlushAlphaList(0f)</c>
|
||
/// @0x0059f30b) — retail's own order runs it BEFORE the portal pass
|
||
/// (<c>CPhysicsPart::Draw(parts, 1)</c>), which in turn runs BEFORE the
|
||
/// shell draw (<c>CPhysicsPart::Draw(parts, 0)</c> — see
|
||
/// <see cref="OnBuildingShellTurn"/>, fired separately, after the portal
|
||
/// pass completes). Default no-op.
|
||
/// </summary>
|
||
void OnBuildingTurn(WalkBuilding building) { }
|
||
|
||
/// <summary>
|
||
/// Fires at the END of <see cref="RetailFrameWalk.DrawBuilding"/>, after
|
||
/// its two-pass portal walk (punches + look-ins, across every active
|
||
/// view) has fully completed — <c>CPhysicsPart::Draw(parts, 0)</c>
|
||
/// @0x0059f331, retail's plain-mesh draw of the building's own shell,
|
||
/// which runs strictly AFTER <c>CPhysicsPart::Draw(parts, 1)</c> (the
|
||
/// portal flavor) per the decomp sequence at @0x0059f30b–0x0059f345. Only
|
||
/// fires when <see cref="OnBuildingTurn"/> also fired (same gate; see its
|
||
/// doc comment) — a degraded-out slot reaches neither. Default no-op.
|
||
/// </summary>
|
||
void OnBuildingShellTurn(WalkBuilding building) { }
|
||
|
||
/// <summary>
|
||
/// Fires when the two-pass portal machinery
|
||
/// (<see cref="WalkBuildingPortals"/>) would draw a far-Z punch fan
|
||
/// (<c>DrawPortalPolyInternal</c> @0x0059bc90, pass 1) — carries the
|
||
/// owning building and the polygon in BUILDING-LOCAL space (as stored on
|
||
/// the emitted <see cref="WalkPortalRef"/>) so a driver can resolve the
|
||
/// world transform itself. The FW1 oracle traces never log punches
|
||
/// (<see cref="RetailFrameWalk"/>'s internal <c>PortalPassSink.OnPunch</c>
|
||
/// stayed a no-op through FW1/FW2 for exactly that reason). Default
|
||
/// no-op.
|
||
/// </summary>
|
||
/// <param name="building">The building whose portal walk emitted the
|
||
/// punch.</param>
|
||
/// <param name="polygon">The portal polygon, building-local.</param>
|
||
/// <param name="activeViewIndex">The active-view index the two-pass walk
|
||
/// is pinned to (retail <c>building_view</c> @0x0059f3bf) — the punch
|
||
/// fan clips against THIS view's slice planes in production.</param>
|
||
void OnPunchGeometry(
|
||
WalkBuilding building, WalkPolygon polygon, int activeViewIndex)
|
||
{ }
|
||
|
||
/// <summary>
|
||
/// Fires once per interior root, at the point <c>PView::DrawCells</c>
|
||
/// @0x005a4840 actually DRAWS the root flood's own cells — NOT where the
|
||
/// <see cref="WalkEventKind.DrawCells"/> <see cref="Emit"/> call for the
|
||
/// SAME flood fires (that one sits at breakpoint-ENTRY order, matching
|
||
/// the FW0 oracle traces; it only RECORDS the flood list). Retail's own
|
||
/// order inside <c>DrawCells</c> is: <c>LScape::draw</c> FIRST
|
||
/// (pc:432719, only when exit views survived — see
|
||
/// <see cref="RetailFrameWalk.DrawLandscape"/> and
|
||
/// <see cref="WalkEventKind.Landscape"/>), then a full depth clear
|
||
/// (pc:432731-432732), then the exit-portal seals (pc:432785-432786) —
|
||
/// BOTH unconditional for an interior root's
|
||
/// own flood, whether or not a landscape turn just ran — and ONLY THEN
|
||
/// the flood's cells far-to-near (shell then contents per cell, same
|
||
/// discipline as <see cref="OnLandscapeCellTurn"/>'s per-cell contents
|
||
/// and a building's look-in). This hook fires at that later point, so
|
||
/// this is where a driver should actually draw <paramref name="cells"/>.
|
||
/// Building look-in floods are UNAFFECTED — retail calls
|
||
/// <c>DrawCells</c> re-entrantly there with <c>ov==0</c> and no
|
||
/// landscape/clear/seal step, so their <see cref="WalkEventKind.DrawCells"/>
|
||
/// <see cref="Emit"/> call still fires at the actual draw point (a
|
||
/// driver may keep drawing those immediately, as before). Default no-op.
|
||
/// </summary>
|
||
void OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells) { }
|
||
}
|