acdream/src/AcDream.App/Rendering/Walk/WalkEvents.cs

178 lines
9.2 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>
/// Installs the exact view set <c>LScape::draw</c> is walking. The same
/// view set gates every landscape object's drawing sphere at its cell
/// turn (<c>Render::viewconeCheck</c>); consumers must copy any data they
/// retain because the walk owns and reuses this object.
/// </summary>
void OnLandscapeViews(WalkPortalView activeViews) { }
/// <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 in two reverse passes: every EnvCell shell first,
/// then every cell object list (the same <c>PView::DrawCells</c>
/// discipline used by 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) { }
}