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

1100 lines
52 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.

using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW stage FW3.2b-1: one cell's or one building's already-queried
/// static content, ready for <see cref="WalkStaticStreamPopulator"/> —
/// caller-built, never read from a retained scene (see
/// <see cref="IWalkFrameWorldData"/>'s doc comment).
/// </summary>
/// <param name="Records">Already-classified <see cref="RenderProjectionRecord"/>s
/// for this turn's cell/building, in the SAME order they must enter the walk
/// stream (never re-sorted downstream — <see cref="WalkStaticStreamPopulator"/>'s
/// own contract). Campaign FW3.4a: a segment INTO <see cref="WalkProductionWorldData"/>'s
/// per-frame arena, not a freshly allocated array — see that type's own doc
/// comment.</param>
/// <param name="TupleLandblockId">The clip-slot-resolving landblock id
/// <c>WbDrawDispatcher.ClassifyEntityForWalk</c> needs per record (FW3.2a's
/// <c>tupleLandblockId</c> parameter) — carried per-turn rather than once per
/// frame because a single frame's cells/buildings can span more than one
/// committed landblock.</param>
internal readonly record struct WalkFrameStaticRecords(
ArraySegment<RenderProjectionRecord> Records, uint TupleLandblockId)
{
public static readonly WalkFrameStaticRecords Empty =
new(ArraySegment<RenderProjectionRecord>.Empty, 0);
}
/// <summary>
/// Campaign FW stage FW3.2b-1: the world-data lookups <see cref="WalkFrameDriver"/>
/// needs at each walk turn, entirely caller-built — the driver reads no
/// retained scene state of its own (mirrors <see cref="WalkStaticStreamPopulator"/>'s
/// own "reads no retained scene state itself" contract one layer up). FW3.2b-2
/// wires the real production implementation (<c>RenderSceneQuery.CopyCellStaticsTo</c>
/// / <c>CopyIndexTo</c> + <see cref="WalkBuildingRegistry"/>); this stage's
/// headless referee tests wire a synthetic fake instead.
/// </summary>
internal interface IWalkFrameWorldData
{
/// <summary>An indoor <c>PView::DrawCells</c> flood cell's (or a building
/// look-in's) static content — <c>RenderProjectionClass.IndoorCellStatic</c>.</summary>
WalkFrameStaticRecords GetCellStatics(uint cellId);
/// <summary>One visited landscape (outdoor) cell's static content —
/// <c>RenderProjectionClass.OutdoorStatic</c>, keyed by the SAME
/// <c>(landblockId &amp; 0xFFFF0000) | (cellIndex+1)</c> id
/// <see cref="IWalkEventSink.OnLandscapeCellTurn"/> computes.</summary>
WalkFrameStaticRecords GetOutdoorStatics(uint cellId);
/// <summary>One building's own exterior shell content (<c>IsBuildingShell</c>
/// records anchored at the building's position cell).</summary>
WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building);
/// <summary>Building-local → world, for transforming a punch polygon
/// before <see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> — the
/// production implementation is <see cref="WalkBuildingRegistry.TryGetEntry"/>'s
/// <c>WorldTransform</c> (FW3.2b-2 wiring).</summary>
Matrix4x4 GetBuildingWorldTransform(WalkBuilding building);
}
/// <summary>
/// Campaign FW stage FW3.2b-1: the leaf GPU-adjacent actions
/// <see cref="WalkFrameDriver"/> calls at walk turns that have no
/// <see cref="OrderedDrawStream"/> submission path YET (sky, terrain, an
/// EnvCell shell, a portal punch fan, the interior depth clear, the exit-
/// portal seals) or that aren't a draw at all (the
/// <see cref="RetailAlphaQueue"/> barrier). Kept as its own seam — rather
/// than folding these into <see cref="WalkFrameDriver"/> directly — so the
/// FW3.2b-1 headless referee suite can wire a fake and prove turn ORDER
/// without standing up the real renderers <c>EnvCellRenderer</c>,
/// <c>TerrainModernRenderer</c>, <c>GameSky</c>, and
/// <c>PortalDepthMaskRenderer.DrawDepthFan</c> — FW3.2b-2's job.
///
/// <para>Stream submission itself (<c>WbDrawDispatcher.PrepareOrderedStream</c>/
/// <c>DrawOrderedRange</c>) is deliberately NOT part of this interface: it is
/// already real, production-tested machinery, so <see cref="WalkFrameDriver"/>
/// calls it directly rather than abstracting a method that would just
/// forward to it one layer deeper.</para>
/// </summary>
internal interface IWalkFrameLeafRenderer
{
/// <summary><c>LScape::draw</c> draws <c>GameSky</c> once per outdoor
/// walk (retail draws it once inside <c>LScape::draw</c>; the CURRENT
/// executor's per-slice call is per-slice-equals-once only for the
/// single-view outdoor case it handles today — the walk driver always
/// calls this exactly once per frame's Landscape turn).</summary>
void DrawSky();
/// <summary><c>LScape::grab_visible_cells</c>'s terrain mesh, once per
/// ACTIVE clip slice — <paramref name="sliceIndex"/> is caller-supplied
/// (<see cref="WalkFrameDriver.Collect"/>'s <c>activeTerrainSliceCount</c>)
/// since FW3.2b-1 does not wire <c>ClipFrameAssembler</c>/
/// <c>ViewconeCuller</c> (FW3.2b-2's job — see plan §FW3.2's dynamic-route
/// survival note). Terrain draws FULLY before any per-cell building/
/// outdoor-static turn in this stage's turn order — an intra-stage
/// simplification of retail's true per-cell <c>DrawLandCell</c>/
/// <c>DrawObjCell</c> interleave, recorded here rather than ported, since
/// terrain itself carries no walk event today.</summary>
void DrawTerrainSlice(int sliceIndex);
/// <summary>One committed cell's EnvCell shell —
/// <c>PView::DrawCells</c>'s <c>DrawEnvCell</c> @0x005a4abe. Retail first
/// draws ALL shells in reverse <c>cell_draw_list</c> order, then starts a
/// second reverse loop for <c>DrawObjCellForDummies</c> @0x005a4b0d.
/// The ordinary interior-root flood and every building look-in flood use
/// this same two-pass discipline.</summary>
void DrawCellShell(uint cellId);
/// <summary>One landscape cell's or building shell's static-owner
/// particle submission, at its own walk turn — see
/// <see cref="WalkFrameEventKind.StaticParticles"/> for the retail
/// positional invariant this carries (the #132 falls containment).</summary>
void DrawStaticParticles(IReadOnlySet<uint> ownerIds);
/// <summary>Draws the packed dynamic occupants of one building look-in
/// cell at that cell's OWN <c>PView::DrawCells</c> turn, then submits the
/// cell's static + dynamic particle owners at the same turn. The route
/// index is assigned in the exact order Collect encountered look-in cell
/// turns and therefore matches the frame product's walk-keyed
/// <c>LookInObject</c> ranges.</summary>
void DrawLookInDynamics(
uint cellId,
int routeIndex,
IReadOnlySet<uint> staticParticleOwnerIds);
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
/// (pc:432731-432732) between the outside stage and the interior root's
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
/// (see that interface's own member of the same name in
/// <c>RetailPViewRenderer.cs</c>, staged there on <c>OutsideViewSlices.Length
/// &gt; 0</c> — an ACKNOWLEDGED approximation of retail's true
/// <c>portalsDrawnCount</c> gate per that file's own comment). This walk
/// driver instead fires unconditionally for every interior root (per the
/// 2026-08-30 decomp correction: the coordinator's directive supersedes
/// the packed path's staged gate — reconcile the two if a firmer
/// <c>portalsDrawnCount</c> reading ever lands). Only called for an
/// INTERIOR root, never outdoors (retail has no depth clear there —
/// <c>portalsDrawnCount</c> never applies to <c>LScape::draw</c>'s own
/// top-level walk).</summary>
void ClearInteriorDepth();
/// <summary>The exit-portal seals (pc:432785-432786) — re-stamping every
/// outside-leading portal's TRUE depth right after
/// <see cref="ClearInteriorDepth"/>, so the aperture the clear just wiped
/// stays occluded by the world beyond it rather than by whatever draws
/// next. Production maps this to the existing seal-fan machinery
/// (<c>RetailPViewRenderer.DrawExitPortalMask</c>/
/// <c>PortalDepthMaskRenderer</c>) — this driver only provides the TURN;
/// the real per-portal fan geometry is FW3.2b-2's job. Only called for an
/// INTERIOR root's own flood, never for a building look-in (those call
/// <c>DrawCells</c> re-entrantly with no clear/seal step) and never
/// outdoors.</summary>
void DrawExitSeals();
/// <summary><c>DrawPortalPolyInternal</c> @0x0059bc90's depth-only far-Z
/// punch fan — pass 1 of the building portal walk.
/// <paramref name="worldPolygon"/> is already transformed building-local
/// → world (<see cref="WalkFrameDriver"/> does the transform via
/// <see cref="IWalkFrameWorldData.GetBuildingWorldTransform"/> at Collect
/// time — see that type's own doc comment). The real implementation is
/// <c>PortalDepthMaskRenderer.DrawDepthFan</c> with <c>forceFarZ</c>
/// (FW3.2b-2 wiring) — Replay calls this at exactly the point Collect
/// recorded it: any content queued ahead of the punch (a preceding
/// cell's/building's contents — never this building's OWN shell, which
/// retail draws only after the whole portal walk completes; see
/// <see cref="WalkFrameDriver"/>'s type doc comment) reaches the GPU
/// first. <paramref name="activeViewIndex"/> is the view the emitting
/// two-pass walk was pinned to (retail <c>building_view =
/// Render::portal_view_num</c> @0x0059f3bf) — production clips the fan by
/// that view's slice planes.</summary>
void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex);
/// <summary><c>RetailAlphaQueue.FlushFartherThan</c>'s <c>DrawBuilding</c>
/// barrier — retail's own call site is
/// <c>D3DPolyRender::FlushAlphaList(0f)</c> @0x0059f30b, a FLUSH-ALL, not
/// a distance-gated flush; this stage keeps the DISPATCHED
/// <c>FlushFartherThan(viewerDistanceTo(building))</c> shape (the two
/// coincide under the walk's far-to-near landscape order, since a nearer
/// emitter has not been inserted into the alpha queue yet — see
/// <c>RetailAlphaQueue.FlushFartherThan</c>'s own doc comment) and flags
/// the 0f/address detail as an FW4 adjudication candidate rather than
/// silently reinterpreting the dispatched design.</summary>
void AlphaBarrier(float viewerDistance);
}
/// <summary>
/// Campaign FW stage FW3.2b-1 test seam: an optional, diagnostic-only
/// observer of every ordered-stream range <see cref="WalkFrameDriver.Replay"/>
/// draws. Production callers pass <see langword="null"/> (the default) — this
/// exists purely so the headless referee suite can assert range COUNT,
/// per-range command count, and per-range stage without re-deriving them
/// from <c>RecordingGpuDevice.Calls</c>' lower-level RHI call log.
/// </summary>
internal interface IWalkFrameDriverTrace
{
/// <summary><paramref name="stages"/> is a snapshot (never a live,
/// about-to-mutate list) of every command's <see cref="WalkDrawStage"/> in
/// the drawn segment, in stream order — by this stage's own segmenting
/// discipline (a segment boundary before every non-stream leaf action) a
/// segment is always single-stage in practice, but the full list is
/// passed so a test can assert that invariant itself instead of trusting
/// it.</summary>
void OnFlush(int commandCount, IReadOnlyList<WalkDrawStage> stages);
}
/// <summary>
/// Campaign FW3.4a: one turn Collect recorded, replayed by
/// <see cref="WalkFrameDriver.Replay"/> in the exact order Collect saw it.
/// <see cref="WalkFrameEventKind.StreamMark"/> is the collect-time analogue of
/// the old immediate driver's flush point — see <see cref="WalkFrameEventKind"/>'s
/// own doc comment for the full list and what each carries.
/// </summary>
internal enum WalkFrameEventKind : byte
{
/// <summary>The accumulated <see cref="OrderedDrawStream"/> grew since the
/// last mark and must be drawn, via <c>WbDrawDispatcher.DrawOrderedRange</c>,
/// before whatever leaf event follows. <see cref="WalkFrameEvent.IntArg"/>
/// is the stream's exclusive-end command index at the moment this event
/// was recorded.</summary>
StreamMark,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawSky"/>.</summary>
Sky,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawTerrainSlice"/> —
/// <see cref="WalkFrameEvent.IntArg"/> is the slice index.</summary>
TerrainSlice,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
/// <see cref="WalkFrameEvent.CellId"/> is the cell.</summary>
CellShell,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> —
/// <see cref="WalkFrameEvent.Polygon"/> is the already-world-transformed
/// polygon (transformed at Collect time, exactly as the pre-FW3.4a driver
/// transformed it before its own immediate call), <see cref="WalkFrameEvent.IntArg"/>
/// is the active view index.</summary>
PunchFan,
/// <summary><see cref="IWalkFrameLeafRenderer.AlphaBarrier"/> —
/// <see cref="WalkFrameEvent.FloatArg"/> is the viewer distance, computed
/// at Collect time (the context that supplies it does not outlive Collect).</summary>
AlphaBarrier,
/// <summary><see cref="IWalkFrameLeafRenderer.ClearInteriorDepth"/>.</summary>
ClearInteriorDepth,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawExitSeals"/>.</summary>
ExitSeals,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawStaticParticles"/> —
/// ONE landscape cell's (<see cref="WalkFrameEvent.CellId"/>) or ONE
/// building shell's (<see cref="WalkFrameEvent.Building"/>) static-owner
/// particle submission, emitted AT ITS OWN WALK TURN. Retail's falls
/// containment is positional: an outdoor emitter's polys join the one
/// alpha list during its owner cell's <c>DrawObjCell</c> in the
/// far-to-near landscape walk, so every nearer building's pre-punch
/// alpha barrier (<c>DrawBuilding</c> @0x0059f2a0's
/// <c>FlushAlphaList</c> @0x0059f30b) drains the already-queued FARTHER
/// content against still-true depth BEFORE the punch stamps far-Z into
/// the aperture. The former single batched submission at the pre-clear
/// closure ran AFTER every punch — the barriers fired over an empty
/// queue and the falls drained against punched-far aperture pixels (the
/// cathedral bleed; the old pipeline's user-verified #132 fix
/// `e102fb36` encoded the same invariant).</summary>
StaticParticles,
/// <summary><see cref="IWalkFrameLeafRenderer.DrawLookInDynamics"/> —
/// <see cref="WalkFrameEvent.CellId"/> is the look-in cell and
/// <see cref="WalkFrameEvent.IntArg"/> is its walk-ordered packed route
/// index.</summary>
LookInDynamics,
}
/// <summary>
/// The exact portal-view cones installed at the walk's building look-in
/// <c>DrawCells</c> turns. Retail <c>RenderDeviceD3D::DrawMesh</c>
/// @0x005A0860 tests each object's drawing sphere against these views before
/// drawing the mesh whole; cell membership alone is not an admission rule.
/// </summary>
internal interface IWalkLookInViewSource
{
IReadOnlyList<uint> LookInCellTurns { get; }
bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius);
}
internal readonly record struct WalkLookInSlice(int PlaneStart, int PlaneCount);
internal readonly record struct WalkLookInTurn(
uint CellId,
int SliceStart,
int SliceCount);
/// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per
/// kind. A single struct (rather than a kind hierarchy) keeps Collect's
/// per-turn list a flat, allocation-cheap <c>List&lt;WalkFrameEvent&gt;</c> —
/// only <see cref="Polygon"/> (a punch fan's already-transformed geometry)
/// allocates, and only once per punch, which is rare enough per frame to be
/// unconditionally acceptable (plan §FW3.4a's own call).</summary>
internal readonly struct WalkFrameEvent
{
private WalkFrameEvent(
WalkFrameEventKind kind, int intArg, uint cellId, float floatArg, WalkPolygon? polygon,
WalkBuilding? building = null)
{
Kind = kind;
IntArg = intArg;
CellId = cellId;
FloatArg = floatArg;
Polygon = polygon;
Building = building;
}
internal WalkFrameEventKind Kind { get; }
internal int IntArg { get; }
internal uint CellId { get; }
internal float FloatArg { get; }
internal WalkPolygon? Polygon { get; }
/// <summary><see cref="WalkFrameEventKind.StaticParticles"/> only: the
/// building whose shell statics' owners submit at this turn; null for a
/// landscape cell's turn (then <see cref="CellId"/> names the cell).</summary>
internal WalkBuilding? Building { get; }
internal static WalkFrameEvent Mark(int exclusiveEnd) =>
new(WalkFrameEventKind.StreamMark, exclusiveEnd, 0, 0f, null);
internal static WalkFrameEvent Sky() =>
new(WalkFrameEventKind.Sky, 0, 0, 0f, null);
internal static WalkFrameEvent TerrainSlice(int sliceIndex) =>
new(WalkFrameEventKind.TerrainSlice, sliceIndex, 0, 0f, null);
internal static WalkFrameEvent CellShell(uint cellId) =>
new(WalkFrameEventKind.CellShell, 0, cellId, 0f, null);
internal static WalkFrameEvent PunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
new(WalkFrameEventKind.PunchFan, activeViewIndex, 0, 0f, worldPolygon);
internal static WalkFrameEvent AlphaBarrier(float viewerDistance) =>
new(WalkFrameEventKind.AlphaBarrier, 0, 0, viewerDistance, null);
internal static WalkFrameEvent LandscapeCellParticles(uint cellId) =>
new(WalkFrameEventKind.StaticParticles, 0, cellId, 0f, null);
internal static WalkFrameEvent BuildingShellParticles(WalkBuilding building) =>
new(WalkFrameEventKind.StaticParticles, 0, 0, 0f, null, building);
internal static WalkFrameEvent LookInDynamics(uint cellId, int routeIndex) =>
new(WalkFrameEventKind.LookInDynamics, routeIndex, cellId, 0f, null);
internal static WalkFrameEvent ClearInteriorDepth() =>
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
internal static WalkFrameEvent ExitSeals() =>
new(WalkFrameEventKind.ExitSeals, 0, 0, 0f, null);
}
/// <summary>
/// Campaign FW stage FW3.2b-1 — THE WALK FRAME DRIVER. Executes one full
/// static-content frame by driving <see cref="RetailFrameWalk"/> with itself
/// as the <see cref="IWalkEventSink"/>, so that GPU command-buffer order
/// equals retail's walk order (plan §FW3.2b-1's "INTERLEAVING RULE").
///
/// <para><b>Campaign FW3.4a — the ONE-walk split.</b> Before this stage, a
/// single frame ran <see cref="RetailFrameWalk"/> TWICE — once with a
/// set-collecting sink to learn the flood/visited-cell set before
/// <c>PrepareCellBatches</c>, once more through this driver to actually
/// submit — and each walk turn's stream content flushed IMMEDIATELY through
/// its own full <c>WbDrawDispatcher.SubmitOrderedStream</c> call (~40 of
/// those per frame at a town, each rewriting and rebinding all nine
/// per-instance sections for that turn's handful of instances). The FW3.4
/// perf checkpoint measured +33.5% CPU p50 and 14× frame allocation from
/// exactly those two costs (plus a third, unrelated one — see
/// <see cref="WalkProductionWorldData"/>'s own doc comment) and tripped the
/// campaign's ±20% stop rule. This stage collapses both: <see cref="Collect"/>
/// runs <see cref="RetailFrameWalk"/> ONCE, doing everything the immediate
/// driver used to do EXCEPT the actual GPU submission — stream appends
/// accumulate without flushing, every former immediate leaf call records a
/// <see cref="WalkFrameEvent"/> instead, and the driver keeps its
/// visited-set bookkeeping (absorbing the renderer's old dedicated
/// set-collecting sink) so the SAME walk answers both questions.
/// <see cref="Replay"/> then performs the actual GPU work afterward:
/// <c>WbDrawDispatcher.PrepareOrderedStream</c> uploads the WHOLE frame's
/// stream once, and each recorded <see cref="WalkFrameEventKind.StreamMark"/>
/// becomes one cheap <c>DrawOrderedRange</c> call over the already-uploaded
/// payload — interleaved, in the exact recorded order, with the leaf
/// renderer calls the OLD immediate driver made inline. Because Replay walks
/// the SAME event sequence Collect recorded at the SAME points the old code
/// flushed, GPU command order is unchanged bit-for-bit; only the number of
/// walks (two → one) and the shape of the GPU submission (many small
/// rebind-and-draw calls → one bind, many cheap draws) changes.
/// <see cref="RunFrame"/> remains Collect immediately followed by Replay, for
/// callers (today: the headless referee suite) that do not need the split;
/// <c>RetailPViewRenderer</c> uses the split directly, since it must run
/// <c>PrepareCellBatches</c>/<c>BuildAndBorrow</c> BETWEEN them.</para>
///
/// <para><b>The one mark rule that reproduces the whole frame script:</b>
/// before EVERY leaf-renderer event (<see cref="WalkFrameEventKind.Sky"/>,
/// <c>TerrainSlice</c>, <c>CellShell</c>, <c>ClearInteriorDepth</c>,
/// <c>ExitSeals</c>, <c>PunchFan</c>) and before every
/// <see cref="WalkFrameEventKind.AlphaBarrier"/> event, Collect records a
/// <see cref="WalkFrameEventKind.StreamMark"/> if the stream grew since the
/// last one (a no-op otherwise — "empty segments submit nothing"); a
/// building's own shell content is APPENDED (not marked) the moment
/// <see cref="IWalkEventSink.OnBuildingShellTurn"/> fires, so it only gets a
/// mark ahead of whatever non-stream event comes next (the next building's
/// alpha barrier, or the final mark at <see cref="Replay"/>'s prepare step).
/// This single rule, combined with retail's two reverse flood passes (ALL
/// shells, then ALL contents),
/// retail's own building order (alpha barrier → portal pass → shell — see
/// <see cref="RetailFrameWalk.DrawBuilding"/>'s doc comment), and retail's
/// own interior-root DRAW order (landscape → clear → seals → the flood's own
/// cells — see <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>'s doc
/// comment; this is NOT the order the walk's EVENTS fire in, which is
/// breakpoint-entry order matching the FW0 oracle traces), is what produces
/// every ordering constraint the plan's frame script names: [far shell] …
/// [near shell] [far contents] … [near contents], [alpha barrier] [punch
/// fan(s) + look-in flood(s), each following the SAME reverse two-pass
/// discipline] [building shell content mark], [landscape (if exit views
/// survived)] [interior depth clear] [exit-portal seals] [the interior
/// root's own flood cells], and a final mark at Replay's prepare step. No
/// special-casing per turn kind is needed beyond that.</para>
///
/// <para>Retail anchors: <c>SmartBox::RenderNormalMode</c> @0x00453aa0 (the
/// root <see cref="RetailFrameWalk.WalkFrame"/> already ports),
/// <c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140 (building-before-
/// DrawObjCell per landscape cell), <c>PView::DrawCells</c> @0x005a4840
/// (the complete reverse <c>DrawEnvCell</c> loop @0x005a4a000x005a4ade
/// before the complete reverse <c>DrawObjCellForDummies</c> loop
/// @0x005a4ade0x005a4b2d; <c>LScape::draw</c> pc:432719, the depth
/// clear pc:432731-432732, and the exit-portal seals pc:432785-432786 —
/// ALL strictly before the flood's own cells, though the event marking
/// <c>DrawCells</c> entry fires before all three), <c>RenderDeviceD3D::DrawBuilding</c>
/// @0x0059f2a0 (the <c>part-&gt;gfxobj[deg_level]!=0</c> gate @0x0059f2d3
/// and the alpha-barrier → portal-pass → shell order @0x0059f30b0x0059f345).</para>
/// </summary>
internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
{
private readonly WbDrawDispatcher _dispatcher;
private readonly WalkStaticStreamPopulator _populator;
private readonly IWalkFrameLeafRenderer _leafRenderer;
private readonly IWalkFrameWorldData _worldData;
private readonly IWalkFrameDriverTrace? _trace;
private readonly OrderedDrawStream _stream = new();
private readonly List<WalkFrameEvent> _events = new();
private readonly List<int> _markPositions = new();
// Campaign FW3.4a: visited-set collection, absorbed from the renderer's
// former dedicated set-collecting sink (RetailPViewRenderer's old
// WalkVisitedSetCollector) — the SAME shapes that sink produced, now
// populated by the ONE walk Collect already runs instead of a second
// walk pass dedicated to nothing but set-gathering.
internal HashSet<uint> VisitedCells { get; } = new();
/// <summary>Building look-in cells in the exact order the walk encountered
/// their <c>DrawCells</c> turns. Duplicates are intentional: two authored
/// portal views can independently visit the same cell and therefore own
/// distinct packed route indices.</summary>
internal List<uint> LookInCellTurns { get; } = new();
private readonly List<WalkLookInTurn> _lookInTurns = new();
private readonly List<WalkLookInSlice> _lookInSlices = new();
private readonly List<WalkPlane> _lookInPlanes = new();
private WalkPlane _lookInCyPlane;
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
/// exclusion and root-flood particle bookkeeping.</summary>
internal HashSet<uint> LookInCells { get; } = new();
/// <summary>FW4 slice 2: the interior root's ORDERED flood cell list,
/// exactly as retail's <c>PView::DrawCells</c> iterates it for the
/// exit-portal seals (pc:432785-432786) — captured at
/// <see cref="IWalkEventSink.OnInteriorFloodDrawTurn"/>. Empty for an
/// outdoor-rooted frame. The production seal draw iterates THIS list
/// (the walk's own flood), not the old visibility apparatus's — at the
/// #456 seam band the old builder's flood misses exit portals the walk
/// provably floods, leaving unsealed aperture depth the end-of-frame
/// alpha drain splats through (the cathedral falls shine-through).</summary>
internal List<uint> InteriorFloodCells { get; } = new();
// Replay scratch for StaticParticles events (sequential replay — one
// reused set is safe).
private readonly HashSet<uint> _staticParticleOwnerScratch = new();
private static void UnionOwners(
in WalkFrameStaticRecords records, HashSet<uint> destination)
{
foreach (RenderProjectionRecord record in records.Records)
{
if (record.Source.LocalEntityId != 0)
destination.Add(record.Source.LocalEntityId);
}
}
internal List<WalkBuilding> VisitedBuildings { get; } = new();
internal HashSet<uint> VisitedLandscapeCellIds { get; } = new();
// ---- transient per-Collect state (set in BeginFrame, read by Replay,
// cleared by Replay's own completion) ----
private IWalkBuildingFrameContext? _ctx;
private Matrix4x4 _viewProjection;
private Vector3 _cameraWorldPosition;
private bool _skyDrawnThisFrame;
private WalkDrawStage? _currentDcStage;
private bool _readyToReplay;
private int _lookInRouteIndex;
internal WalkFrameDriver(
WbDrawDispatcher dispatcher,
IWalkFrameLeafRenderer leafRenderer,
IWalkFrameWorldData worldData,
IWalkFrameDriverTrace? trace = null)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_leafRenderer = leafRenderer ?? throw new ArgumentNullException(nameof(leafRenderer));
_worldData = worldData ?? throw new ArgumentNullException(nameof(worldData));
_trace = trace;
_populator = new WalkStaticStreamPopulator(dispatcher);
}
/// <summary>
/// Drives one complete frame at retail's root (<c>SmartBox::RenderNormalMode</c>):
/// <see cref="Collect"/> immediately followed by <see cref="Replay"/>. Kept
/// for callers that don't need the split (today: the headless referee
/// suite) — <c>RetailPViewRenderer</c> calls the pair directly, since it
/// must run other frame work BETWEEN them (plan §FW3.4a).
/// </summary>
internal void RunFrame(
RetailFrameWalk walk,
uint cameraCellId,
WalkCell? cameraCell,
WalkLandscape landscape,
IRetailFrameWalkContext ctx,
IGpuFrame frame,
IGpuPassEncoder encoder,
Matrix4x4 viewProjection,
Vector3 cameraWorldPosition)
{
ArgumentNullException.ThrowIfNull(frame);
ArgumentNullException.ThrowIfNull(encoder);
Collect(
walk, cameraCellId, cameraCell, landscape, ctx,
viewProjection, cameraWorldPosition);
Replay(frame, encoder);
}
/// <summary>
/// Campaign FW3.4a Phase 1 — THE ONE WALK. Drives
/// <see cref="RetailFrameWalk.WalkFrame"/> with this driver as its sink,
/// sandwiched between <see cref="BeginFrame"/>/<see cref="EndFrame"/>,
/// performing NO GPU work: see this type's own doc comment.
/// </summary>
internal void Collect(
RetailFrameWalk walk,
uint cameraCellId,
WalkCell? cameraCell,
WalkLandscape landscape,
IRetailFrameWalkContext ctx,
Matrix4x4 viewProjection,
Vector3 cameraWorldPosition)
{
ArgumentNullException.ThrowIfNull(walk);
ArgumentNullException.ThrowIfNull(landscape);
ArgumentNullException.ThrowIfNull(ctx);
BeginFrame(ctx, viewProjection, cameraWorldPosition);
walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this);
EndFrame();
}
/// <summary>
/// Opens a collect scope without driving the walk itself — for a caller
/// (or a test) that already holds an isolated walk entry point (e.g. one
/// <see cref="RetailFrameWalk.DrawBuilding"/> or
/// <see cref="RetailFrameWalk.DrawLandscape"/> call) and wants this
/// driver's turn handling without going through the top-level root.
/// <see cref="Collect"/> is implemented in terms of this pair. Performs no
/// GPU work — see this type's own doc comment.
/// </summary>
internal void BeginFrame(
IRetailFrameWalkContext ctx,
Matrix4x4 viewProjection,
Vector3 cameraWorldPosition)
{
ArgumentNullException.ThrowIfNull(ctx);
if (_ctx is not null)
{
throw new InvalidOperationException(
"WalkFrameDriver.BeginFrame was called while a previous frame was still open — "
+ "the driver is not re-entrant (Campaign FW3.2b-1 fail-loud rule); call "
+ "EndFrame (or let a thrown exception's cleanup run) before starting the next.");
}
_ctx = ctx;
_viewProjection = viewProjection;
_cameraWorldPosition = cameraWorldPosition;
_skyDrawnThisFrame = false;
_currentDcStage = null;
_readyToReplay = false;
_stream.Reset();
_events.Clear();
_markPositions.Clear();
VisitedCells.Clear();
LookInCellTurns.Clear();
_lookInTurns.Clear();
_lookInSlices.Clear();
_lookInPlanes.Clear();
_lookInCyPlane = ctx.CyPlane;
LookInCells.Clear();
VisitedBuildings.Clear();
VisitedLandscapeCellIds.Clear();
InteriorFloodCells.Clear();
_lookInRouteIndex = 0;
}
/// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame
/// end: final segment flush", now a mark rather than a draw — see this
/// type's own doc comment), then closes the collect scope. The recorded
/// stream/events survive this call — <see cref="Replay"/> consumes them —
/// which is the one behavioral difference from the pre-FW3.4a EndFrame,
/// which reset the stream here because it had just drawn it.</summary>
internal void EndFrame()
{
try
{
MarkIfGrown();
}
finally
{
_ctx = null;
_readyToReplay = true;
}
}
/// <summary>
/// Campaign FW3.4a Phase 2. Requires a completed Collect (an
/// <see cref="EndFrame"/> having run since the last Replay) — throws
/// otherwise, rather than silently replaying a stale or empty event list.
/// Uploads the WHOLE collected stream exactly once (skipped when it is
/// empty), then walks the recorded events in order: a
/// <see cref="WalkFrameEventKind.StreamMark"/> issues one
/// <c>WbDrawDispatcher.DrawOrderedRange</c> call over the segment it
/// closes off; every other event kind issues its corresponding
/// <see cref="IWalkFrameLeafRenderer"/> call. Because Collect recorded
/// these events at EXACTLY the points the pre-FW3.4a immediate driver
/// used to flush/draw, this reproduces the SAME interleaved GPU command
/// order — the campaign invariant — from one walk instead of two.
/// </summary>
internal void Replay(IGpuFrame frame, IGpuPassEncoder encoder)
{
ArgumentNullException.ThrowIfNull(frame);
ArgumentNullException.ThrowIfNull(encoder);
if (!_readyToReplay)
{
throw new InvalidOperationException(
"WalkFrameDriver.Replay was called without a completed Collect (BeginFrame/"
+ "EndFrame, or Collect/RunFrame) preceding it — there is nothing recorded to "
+ "replay.");
}
if (_stream.Count > 0)
_dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions);
int cursor = 0;
for (int i = 0; i < _events.Count; i++)
{
WalkFrameEvent e = _events[i];
switch (e.Kind)
{
case WalkFrameEventKind.StreamMark:
int end = e.IntArg;
int count = end - cursor;
if (_trace is not null)
_trace.OnFlush(count, _stream.Stages.GetRange(cursor, count));
_dispatcher.DrawOrderedRange(encoder, cursor, count);
cursor = end;
break;
case WalkFrameEventKind.Sky:
_leafRenderer.DrawSky();
break;
case WalkFrameEventKind.TerrainSlice:
_leafRenderer.DrawTerrainSlice(e.IntArg);
break;
case WalkFrameEventKind.CellShell:
_leafRenderer.DrawCellShell(e.CellId);
break;
case WalkFrameEventKind.PunchFan:
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
break;
case WalkFrameEventKind.AlphaBarrier:
_leafRenderer.AlphaBarrier(e.FloatArg);
break;
case WalkFrameEventKind.ClearInteriorDepth:
_leafRenderer.ClearInteriorDepth();
break;
case WalkFrameEventKind.ExitSeals:
_leafRenderer.DrawExitSeals();
break;
case WalkFrameEventKind.StaticParticles:
_staticParticleOwnerScratch.Clear();
UnionOwners(
e.Building is WalkBuilding shellOwner
? _worldData.GetBuildingShellStatics(shellOwner)
: _worldData.GetOutdoorStatics(e.CellId),
_staticParticleOwnerScratch);
if (_staticParticleOwnerScratch.Count > 0)
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
break;
case WalkFrameEventKind.LookInDynamics:
_staticParticleOwnerScratch.Clear();
UnionOwners(
_worldData.GetCellStatics(e.CellId),
_staticParticleOwnerScratch);
_leafRenderer.DrawLookInDynamics(
e.CellId,
e.IntArg,
_staticParticleOwnerScratch);
break;
}
}
_stream.Reset();
_events.Clear();
_markPositions.Clear();
_readyToReplay = false;
}
// ------------------------------------------------------------------
// IWalkEventSink
// ------------------------------------------------------------------
void IWalkEventSink.Emit(in WalkEvent walkEvent)
{
switch (walkEvent.Kind)
{
case WalkEventKind.DrawInside:
_currentDcStage = WalkDrawStage.CellStatic;
VisitedCells.Add(walkEvent.CellId);
break;
case WalkEventKind.Landscape:
HandleLandscapeTurn(walkEvent.OutsideViewCount);
break;
case WalkEventKind.DrawCells:
foreach (uint id in walkEvent.Cells)
VisitedCells.Add(id);
HandleDrawCellsTurn(walkEvent.Cells);
break;
case WalkEventKind.Building:
// OnBuildingTurn (below) carries the actual side effects —
// this vocabulary-only event needs no driver action.
break;
}
}
void IWalkEventSink.OnLandscapeCellTurn(uint cellId)
{
RequireOpenFrame();
VisitedLandscapeCellIds.Add(cellId);
WalkFrameStaticRecords records = _worldData.GetOutdoorStatics(cellId);
_populator.PopulateOutdoorStatics(
_stream, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection);
// FW4 (the #132 positional invariant): this cell's emitter owners
// submit AT THIS TURN, so nearer buildings' pre-punch barriers
// drain them against still-true depth — see
// WalkFrameEventKind.StaticParticles. Mark first so the cell's own
// meshes flush ahead of its particle submission (retail's
// per-object DrawObjCell order).
if (HasAnyOwner(records))
{
MarkIfGrown();
_events.Add(WalkFrameEvent.LandscapeCellParticles(cellId));
}
}
private static bool HasAnyOwner(in WalkFrameStaticRecords records)
{
foreach (RenderProjectionRecord record in records.Records)
{
if (record.Source.LocalEntityId != 0)
return true;
}
return false;
}
void IWalkEventSink.OnBuildingTurn(WalkBuilding building)
{
ArgumentNullException.ThrowIfNull(building);
IWalkBuildingFrameContext ctx = RequireOpenFrame();
VisitedBuildings.Add(building);
// D3DPolyRender::FlushAlphaList(0f) @0x0059f30b — retail's alpha
// barrier, first inside the gate. The portal pass (punches +
// look-ins) follows this call; the building's own shell content is
// appended only once that pass completes (OnBuildingShellTurn).
MarkIfGrown();
_events.Add(WalkFrameEvent.AlphaBarrier(ctx.ViewerDistanceTo(building)));
_currentDcStage = WalkDrawStage.LookInStatic;
}
void IWalkEventSink.OnBuildingShellTurn(WalkBuilding building)
{
ArgumentNullException.ThrowIfNull(building);
RequireOpenFrame();
// CPhysicsPart::Draw(parts, 0) @0x0059f331 — retail's plain-mesh
// shell draw, strictly after the portal pass (CPhysicsPart::Draw
// (parts, 1)). Mark first so this building's shell content never
// shares a replayed range with whatever the portal pass's last
// look-in flood appended (keeps every range single-stage).
MarkIfGrown();
WalkFrameStaticRecords shell = _worldData.GetBuildingShellStatics(building);
_populator.PopulateCell(
_stream, WalkDrawStage.BuildingShell, building.PositionCellId,
shell.Records, shell.TupleLandblockId, _cameraWorldPosition, _viewProjection);
// FW4 (the #132 positional invariant): the building's own shell
// emitters submit at the shell turn, after the shell content
// flushes — see WalkFrameEventKind.StaticParticles.
if (HasAnyOwner(shell))
{
MarkIfGrown();
_events.Add(WalkFrameEvent.BuildingShellParticles(building));
}
}
void IWalkEventSink.OnPunchGeometry(
WalkBuilding building, WalkPolygon polygon, int activeViewIndex)
{
ArgumentNullException.ThrowIfNull(building);
ArgumentNullException.ThrowIfNull(polygon);
RequireOpenFrame();
MarkIfGrown();
Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building);
_events.Add(
WalkFrameEvent.PunchFan(TransformToWorld(polygon, worldTransform), activeViewIndex));
}
void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells)
{
ArgumentNullException.ThrowIfNull(cells);
RequireOpenFrame();
// PView::DrawCells @0x005a4840: the gated 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 (see this driver's type doc
// comment).
MarkIfGrown();
_events.Add(WalkFrameEvent.ClearInteriorDepth());
MarkIfGrown();
_events.Add(WalkFrameEvent.ExitSeals());
// FW4 slice 2: retain the ordered flood for the seal draw (the
// DrawExitSeals leaf runs at Replay, when Collect has long filled
// this) — see the property's own doc comment.
InteriorFloodCells.Clear();
for (int i = 0; i < cells.Count; i++)
InteriorFloodCells.Add(cells[i]);
EmitFloodTurns(WalkDrawStage.CellStatic, cells);
}
// ------------------------------------------------------------------
// Turn handlers
// ------------------------------------------------------------------
private void HandleLandscapeTurn(int activeViewCount)
{
RequireOpenFrame();
if (activeViewCount < 1)
{
throw new InvalidOperationException(
$"A Landscape turn fired with {activeViewCount} active views — "
+ "RetailFrameWalk only draws the landscape through an installed view set "
+ "(the outdoor root's full-screen default view, or an interior root's "
+ "surviving exit views, both at least 1). A zero/negative count is a "
+ "walk/driver desync (Campaign FW fail-loud rule).");
}
if (_skyDrawnThisFrame)
{
throw new InvalidOperationException(
"A second Landscape turn fired in one frame — RetailFrameWalk.WalkFrame/"
+ "DrawInside's own call graph guarantees at most one Landscape turn per "
+ "frame (outdoor root draws it once; an interior root draws it at most "
+ "once more, through surviving exit views). A second occurrence is a walk/"
+ "driver desync, not something to silently double-draw sky for (Campaign "
+ "FW3.2b-1 fail-loud rule).");
}
MarkIfGrown();
_events.Add(WalkFrameEvent.Sky());
_skyDrawnThisFrame = true;
// FW4 slice 6 (correcting slice 1's per-view fan): retail's
// LScape::draw draws the terrain blocks ONCE per landscape turn —
// the active views feed only the block-level visibility union
// (CheckBlocks); terrain cells are ordinary meshes and retail never
// clips those per view (pixel exactness = the depth clear + seals +
// interior repaint afterward). One terrain turn, always; the walk's
// views still own the punch fans and dynamics apertures.
_events.Add(WalkFrameEvent.TerrainSlice(0));
}
private void HandleDrawCellsTurn(IReadOnlyList<uint> cells)
{
RequireOpenFrame();
if (_currentDcStage is not { } stage)
{
throw new InvalidOperationException(
"A DrawCells turn fired before any DrawInside or Building turn established "
+ "which stage its cells belong to — a walk/driver desync (Campaign FW3.2b-1 "
+ "fail-loud rule): RetailFrameWalk only ever emits DrawCells after DrawInside "
+ "(the interior root's own flood) or after a building's look-in portal pass.");
}
if (stage == WalkDrawStage.CellStatic)
{
// The interior root's OWN flood: this DC event fired at
// breakpoint-ENTRY order (matching the FW0 oracle traces), not
// retail's actual draw point — RECORD only. RetailFrameWalk.
// DrawInside hands the SAME cell list to
// OnInteriorFloodDrawTurn later, at the point retail really
// draws it (after the landscape turn, or immediately when no
// exit views survived) — see that method's doc comment.
return;
}
// Any other stage (LookInStatic) is a building's look-in flood:
// retail calls DrawCells re-entrantly there with no landscape/clear/
// seal step, so its DC event already fires at the real draw point —
// record immediately. PView::DrawCells uses two complete reverse
// loops: every EnvCell shell first, then every cell object list.
EmitFloodTurns(stage, cells);
}
private void EmitFloodTurns(WalkDrawStage stage, IReadOnlyList<uint> cells)
{
// PView::DrawCells @0x005A4840, loop 2 (005A4A00005A4ADE):
// cell_draw_list[count - 1] down to zero, DrawEnvCell only.
for (int i = cells.Count - 1; i >= 0; i--)
{
MarkIfGrown();
_events.Add(WalkFrameEvent.CellShell(cells[i]));
}
// Loop 3 (005A4ADE005A4B2D): restart at count - 1 and draw each
// complete object cell. Keep a stream mark per cell so the packed
// static records and its dynamic/particle tail stay at that exact
// retail turn.
for (int i = cells.Count - 1; i >= 0; i--)
EmitCellContentsTurn(stage, cells[i]);
}
private void EmitCellContentsTurn(WalkDrawStage stage, uint cellId)
{
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
_populator.PopulateCell(
_stream, stage, cellId, records.Records, records.TupleLandblockId,
_cameraWorldPosition, _viewProjection);
MarkIfGrown();
if (stage == WalkDrawStage.LookInStatic)
{
// Retail draws a look-in cell's complete object list at this
// re-entrant DrawCells turn. The packed dynamic route used to run
// much later at the pre-clear boundary, after nearer building
// shells, which let the cathedral's 0x112 remote player overpaint
// opaque walls. Keep animation/fade in the packed route, but replay
// it here between this cell's content and the building shell.
int routeIndex = _lookInRouteIndex++;
LookInCellTurns.Add(cellId);
LookInCells.Add(cellId);
CaptureLookInViews(cellId);
_events.Add(WalkFrameEvent.LookInDynamics(cellId, routeIndex));
}
}
private void CaptureLookInViews(uint cellId)
{
IWalkBuildingFrameContext ctx = RequireOpenFrame();
WalkCell? cell = ctx.GetVisible(cellId);
if (cell is null || cell.NumView <= 0)
{
_lookInTurns.Add(new WalkLookInTurn(
cellId, _lookInSlices.Count, 0));
return;
}
WalkPortalView portalView = cell.TopView;
int sliceStart = _lookInSlices.Count;
for (int sliceIndex = 0; sliceIndex < portalView.ViewCount; sliceIndex++)
{
WalkViewPoly poly = portalView.View.Polys[sliceIndex];
int planeStart = _lookInPlanes.Count;
for (int edge = 0; edge < poly.VertexCount; edge++)
{
_lookInPlanes.Add(
portalView.View.Vertices[poly.VertexIndex + edge].Plane);
}
_lookInSlices.Add(new WalkLookInSlice(
planeStart, poly.VertexCount));
}
_lookInTurns.Add(new WalkLookInTurn(
cellId, sliceStart, _lookInSlices.Count - sliceStart));
}
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
return false;
WalkLookInTurn turn = _lookInTurns[routeIndex];
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
{
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
if (WalkVisibilityMath.ViewconeCheck(
center,
radius,
_lookInCyPlane,
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
slice.PlaneStart,
slice.PlaneCount)) != WalkBoundingType.Outside)
{
return true;
}
}
return false;
}
/// <summary>Campaign FW3.4a: the collect-time analogue of the old
/// immediate driver's <c>FlushIfNonEmpty</c> — records a
/// <see cref="WalkFrameEventKind.StreamMark"/> at the stream's current
/// length if it grew since the last mark (a no-op otherwise, exactly
/// like that method's own "empty segments submit nothing" rule). Also
/// appends the boundary to <see cref="_markPositions"/>, which
/// <see cref="Replay"/> hands to <c>PrepareOrderedStream</c> so a merge
/// run can never span it — see <c>WbDrawDispatcher.BuildOrderedMergeRuns</c>'s
/// <c>forcedBreaksAscending</c> parameter for why that matters.</summary>
private void MarkIfGrown()
{
int count = _stream.Count;
int last = _markPositions.Count > 0 ? _markPositions[^1] : 0;
if (count == last)
return;
_markPositions.Add(count);
_events.Add(WalkFrameEvent.Mark(count));
}
private IWalkBuildingFrameContext RequireOpenFrame() =>
_ctx ?? throw new InvalidOperationException(
"WalkFrameDriver received a walk turn outside BeginFrame/EndFrame — call "
+ "BeginFrame (or Collect/RunFrame) before driving the walk with this driver as "
+ "its IWalkEventSink.");
/// <summary><c>ConstructBuildingView</c>'s polygon is building-local; the
/// punch fan needs world space. Vertices transform directly; the plane
/// normal uses <see cref="Vector3.TransformNormal"/> (correct for the
/// rigid, shear-free placements <see cref="WalkBuildingFactory"/> and
/// <see cref="WalkCellFactory"/> build) and <c>D</c> is rederived from
/// the transformed normal and the first transformed vertex — the SAME
/// construction <see cref="WalkCellFactory"/>/<see cref="WalkBuildingFactory"/>
/// already use for their own polygons (<c>Plane = new WalkPlane(normal,
/// -Vector3.Dot(normal, vertices[0]))</c>).</summary>
private static WalkPolygon TransformToWorld(WalkPolygon local, Matrix4x4 worldTransform)
{
var vertices = new Vector3[local.Vertices.Length];
for (int i = 0; i < vertices.Length; i++)
vertices[i] = Vector3.Transform(local.Vertices[i], worldTransform);
Vector3 normal = local.Vertices.Length > 0
? Vector3.Normalize(Vector3.TransformNormal(local.Plane.Normal, worldTransform))
: Vector3.Zero;
float d = vertices.Length > 0 ? -Vector3.Dot(normal, vertices[0]) : 0f;
return new WalkPolygon { Vertices = vertices, Plane = new WalkPlane(normal, d) };
}
}