acdream/src/AcDream.App/Rendering/Walk/WalkEvents.cs
Erik 37febd1fe6 fix(render): FW4 slice 1 - interior outside-view slices come from the walk
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>
2026-08-30 18:21:08 +02:00

170 lines
8.9 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.

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 &amp; 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-&gt;gfxobj[part-&gt;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 @0x0059f30b0x0059f345. 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) { }
}