acdream/src/AcDream.App/Rendering/Walk/WalkEvents.cs
Erik 1b59eae428 feat(render) Campaign FW3.2b-2 step 1: the production leaf adapter
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>
2026-08-30 14:40:24 +02:00

161 lines
8.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)
{
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 &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) { }
}