The FW3 visual gate's stairwell/grass transition flash (grass briefly covering floor openings at doorway crossings - the #119 family) was the FW3 dual path leaking: the walk decided WHETHER terrain draws while the old PortalVisibilityBuilder assembly decided WHERE (slice planes, count, scissor), and punch fans indexed the old slice array with walk view indices. The new ACDREAM_PROBE_WALK_ROOT apparatus pinned the boundary frames: fat/degenerate old-apparatus exit views splash terrain over interior pixels, the interior depth-clear preserves color, and cells absent from the walk's flood never repaint. Retail has ONE visibility structure and cannot produce this. ClipFrameAssembler.ReassembleOutsideViewFromWalk now materializes the walk's own outside_view (pixel screen points -> standard NDC -> the existing ClipPlaneSet machinery) into the assembly's outside-view block after Collect, ahead of the single PrepareClipFrame publication (moved below the walk block). The Landscape event carries the walk's active view count on the record's existing OutsideViewCount field (trace mapping compares kind only - zero oracle-fixture churn) and the driver fans exactly that many terrain slices; activeTerrainSliceCount is deleted end to end. Outdoor roots keep the assembler's single full-screen slice, asserted ==1. Hermetic 6,762/0 (4 new materializer tests pin the y-flip and plane-sign conventions), Walk lane 209/1, InstalledDat walk conformance 40/1. Seals/cell slices/look-in seeding stay on the old per-cell views for the rest of FW4 (identical dat polygons; only the visible set can differ). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
170 lines
8.9 KiB
C#
170 lines
8.9 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)
|
||
{
|
||
/// <summary>FW4 slice 1: the landscape turn carries the ACTIVE view
|
||
/// set's <c>ViewCount</c> (retail <c>LScape::draw</c> iterates the
|
||
/// views the walk itself installed — 1 for the outdoor root's
|
||
/// full-screen default view, the interior root's own surviving
|
||
/// exit-view count otherwise). The driver fans exactly this many
|
||
/// terrain slices; the old clip apparatus no longer supplies the
|
||
/// count. Reuses the record's existing <see cref="WalkEvent.OutsideViewCount"/>
|
||
/// field; the conformance trace mapping compares the kind only
|
||
/// (<c>LS</c>), so oracle fixtures are unaffected.</summary>
|
||
public static WalkEvent Landscape(int activeViewCount)
|
||
=> new(WalkEventKind.Landscape, 0, activeViewCount, 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) { }
|
||
}
|