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; /// /// Campaign FW stage FW3.2b-1: one cell's or one building's already-queried /// static content, ready for — /// caller-built, never read from a retained scene (see /// 's doc comment). /// /// Already-classified s /// for this turn's cell/building, in the SAME order they must enter the walk /// stream (never re-sorted downstream — 's /// own contract). Campaign FW3.4a: a segment INTO 's /// per-frame arena, not a freshly allocated array — see that type's own doc /// comment. /// The clip-slot-resolving landblock id /// WbDrawDispatcher.ClassifyEntityForWalk needs per record (FW3.2a's /// tupleLandblockId parameter) — carried per-turn rather than once per /// frame because a single frame's cells/buildings can span more than one /// committed landblock. internal readonly record struct WalkFrameStaticRecords( ArraySegment Records, uint TupleLandblockId) { public static readonly WalkFrameStaticRecords Empty = new(ArraySegment.Empty, 0); } /// /// Campaign FW stage FW3.2b-1: the world-data lookups /// needs at each walk turn, entirely caller-built — the driver reads no /// retained scene state of its own (mirrors 's /// own "reads no retained scene state itself" contract one layer up). FW3.2b-2 /// wires the real production implementation (RenderSceneQuery.CopyCellStaticsTo /// / CopyIndexTo + ); this stage's /// headless referee tests wire a synthetic fake instead. /// internal interface IWalkFrameWorldData { /// An indoor PView::DrawCells flood cell's (or a building /// look-in's) static content — RenderProjectionClass.IndoorCellStatic. WalkFrameStaticRecords GetCellStatics(uint cellId); /// One visited landscape (outdoor) cell's static content — /// RenderProjectionClass.OutdoorStatic, keyed by the SAME /// (landblockId & 0xFFFF0000) | (cellIndex+1) id /// computes. WalkFrameStaticRecords GetOutdoorStatics(uint cellId); /// One building's own exterior shell content (IsBuildingShell /// records anchored at the building's position cell). WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building); /// Building-local → world, for transforming a punch polygon /// before — the /// production implementation is 's /// WorldTransform (FW3.2b-2 wiring). Matrix4x4 GetBuildingWorldTransform(WalkBuilding building); } /// /// Campaign FW stage FW3.2b-1: the leaf GPU-adjacent actions /// calls at walk turns that have no /// 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 /// barrier). Kept as its own seam — rather /// than folding these into directly — so the /// FW3.2b-1 headless referee suite can wire a fake and prove turn ORDER /// without standing up the real renderers EnvCellRenderer, /// TerrainModernRenderer, GameSky, and /// PortalDepthMaskRenderer.DrawDepthFan — FW3.2b-2's job. /// /// Stream submission itself (WbDrawDispatcher.PrepareOrderedStream/ /// DrawOrderedRange) is deliberately NOT part of this interface: it is /// already real, production-tested machinery, so /// calls it directly rather than abstracting a method that would just /// forward to it one layer deeper. /// internal interface IWalkFrameLeafRenderer { /// LScape::draw draws GameSky once per outdoor /// walk (retail draws it once inside LScape::draw; 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). void DrawSky(); /// LScape::grab_visible_cells's terrain mesh, once per /// ACTIVE clip slice — is caller-supplied /// ('s activeTerrainSliceCount) /// since FW3.2b-1 does not wire ClipFrameAssembler/ /// ViewconeCuller (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 DrawLandCell/ /// DrawObjCell interleave, recorded here rather than ported, since /// terrain itself carries no walk event today. void DrawTerrainSlice(int sliceIndex); /// One committed cell's EnvCell shell — /// PView::DrawCells's DrawEnvCell @0x005a4abe. Retail first /// draws ALL shells in reverse cell_draw_list order, then starts a /// second reverse loop for DrawObjCellForDummies @0x005a4b0d. /// The ordinary interior-root flood and every building look-in flood use /// this same two-pass discipline. void DrawCellShell(uint cellId); /// One landscape cell's or building shell's static-owner /// particle submission, at its own walk turn — see /// for the retail /// positional invariant this carries (the #132 falls containment). void DrawStaticParticles(IReadOnlySet ownerIds); /// Draws the packed dynamic occupants of one building look-in /// cell at that cell's OWN PView::DrawCells 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 /// LookInObject ranges. void DrawLookInDynamics( uint cellId, int routeIndex, IReadOnlySet staticParticleOwnerIds); /// PView::DrawCells @0x005a4840's gated full depth clear /// (pc:432731-432732) between the outside stage and the interior root's /// own flood — production maps this to IWorldPassScope.ClearInteriorDepth /// (see that interface's own member of the same name in /// RetailPViewRenderer.cs, staged there on OutsideViewSlices.Length /// > 0 — an ACKNOWLEDGED approximation of retail's true /// portalsDrawnCount 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 /// portalsDrawnCount reading ever lands). Only called for an /// INTERIOR root, never outdoors (retail has no depth clear there — /// portalsDrawnCount never applies to LScape::draw's own /// top-level walk). void ClearInteriorDepth(); /// The exit-portal seals (pc:432785-432786) — re-stamping every /// outside-leading portal's TRUE depth right after /// , 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 /// (RetailPViewRenderer.DrawExitPortalMask/ /// PortalDepthMaskRenderer) — 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 /// DrawCells re-entrantly with no clear/seal step) and never /// outdoors. void DrawExitSeals(); /// DrawPortalPolyInternal @0x0059bc90's depth-only far-Z /// punch fan — pass 1 of the building portal walk. /// is already transformed building-local /// → world ( does the transform via /// at Collect /// time — see that type's own doc comment). The real implementation is /// PortalDepthMaskRenderer.DrawDepthFan with forceFarZ /// (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 /// 's type doc comment) reaches the GPU /// first. is the view the emitting /// two-pass walk was pinned to (retail building_view = /// Render::portal_view_num @0x0059f3bf) — production clips the fan by /// that view's slice planes. void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex); /// RetailAlphaQueue.FlushFartherThan's DrawBuilding /// barrier — retail's own call site is /// D3DPolyRender::FlushAlphaList(0f) @0x0059f30b, a FLUSH-ALL, not /// a distance-gated flush; this stage keeps the DISPATCHED /// FlushFartherThan(viewerDistanceTo(building)) 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 /// RetailAlphaQueue.FlushFartherThan's own doc comment) and flags /// the 0f/address detail as an FW4 adjudication candidate rather than /// silently reinterpreting the dispatched design. void AlphaBarrier(float viewerDistance); } /// /// Campaign FW stage FW3.2b-1 test seam: an optional, diagnostic-only /// observer of every ordered-stream range /// draws. Production callers pass (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 RecordingGpuDevice.Calls' lower-level RHI call log. /// internal interface IWalkFrameDriverTrace { /// is a snapshot (never a live, /// about-to-mutate list) of every command's 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. void OnFlush(int commandCount, IReadOnlyList stages); } /// /// Campaign FW3.4a: one turn Collect recorded, replayed by /// in the exact order Collect saw it. /// is the collect-time analogue of /// the old immediate driver's flush point — see 's /// own doc comment for the full list and what each carries. /// internal enum WalkFrameEventKind : byte { /// The accumulated grew since the /// last mark and must be drawn, via WbDrawDispatcher.DrawOrderedRange, /// before whatever leaf event follows. /// is the stream's exclusive-end command index at the moment this event /// was recorded. StreamMark, /// . Sky, /// — /// is the slice index. TerrainSlice, /// — /// is the cell. CellShell, /// — /// is the already-world-transformed /// polygon (transformed at Collect time, exactly as the pre-FW3.4a driver /// transformed it before its own immediate call), /// is the active view index. PunchFan, /// — /// is the viewer distance, computed /// at Collect time (the context that supplies it does not outlive Collect). AlphaBarrier, /// . ClearInteriorDepth, /// . ExitSeals, /// — /// ONE landscape cell's () or ONE /// building shell's () 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 DrawObjCell in the /// far-to-near landscape walk, so every nearer building's pre-punch /// alpha barrier (DrawBuilding @0x0059f2a0's /// FlushAlphaList @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). StaticParticles, /// — /// is the look-in cell and /// is its walk-ordered packed route /// index. LookInDynamics, } /// /// The exact portal-view cones installed at the walk's building look-in /// DrawCells turns. Retail RenderDeviceD3D::DrawMesh /// @0x005A0860 tests each object's drawing sphere against these views before /// drawing the mesh whole; cell membership alone is not an admission rule. /// internal interface IWalkLookInViewSource { IReadOnlyList 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); /// See 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 List<WalkFrameEvent> — /// only (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). 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; } /// only: the /// building whose shell statics' owners submit at this turn; null for a /// landscape cell's turn (then names the cell). 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); } /// /// Campaign FW stage FW3.2b-1 — THE WALK FRAME DRIVER. Executes one full /// static-content frame by driving with itself /// as the , so that GPU command-buffer order /// equals retail's walk order (plan §FW3.2b-1's "INTERLEAVING RULE"). /// /// Campaign FW3.4a — the ONE-walk split. Before this stage, a /// single frame ran TWICE — once with a /// set-collecting sink to learn the flood/visited-cell set before /// PrepareCellBatches, once more through this driver to actually /// submit — and each walk turn's stream content flushed IMMEDIATELY through /// its own full WbDrawDispatcher.SubmitOrderedStream 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 /// 's own doc comment) and tripped the /// campaign's ±20% stop rule. This stage collapses both: /// runs 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 /// 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. /// then performs the actual GPU work afterward: /// WbDrawDispatcher.PrepareOrderedStream uploads the WHOLE frame's /// stream once, and each recorded /// becomes one cheap DrawOrderedRange 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. /// remains Collect immediately followed by Replay, for /// callers (today: the headless referee suite) that do not need the split; /// RetailPViewRenderer uses the split directly, since it must run /// PrepareCellBatches/BuildAndBorrow BETWEEN them. /// /// The one mark rule that reproduces the whole frame script: /// before EVERY leaf-renderer event (, /// TerrainSlice, CellShell, ClearInteriorDepth, /// ExitSeals, PunchFan) and before every /// event, Collect records a /// 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 /// 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 '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 /// 's doc comment), and retail's /// own interior-root DRAW order (landscape → clear → seals → the flood's own /// cells — see '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. /// /// Retail anchors: SmartBox::RenderNormalMode @0x00453aa0 (the /// root already ports), /// RenderDeviceD3D::DrawSortCell @0x0059f140 (building-before- /// DrawObjCell per landscape cell), PView::DrawCells @0x005a4840 /// (the complete reverse DrawEnvCell loop @0x005a4a00–0x005a4ade /// before the complete reverse DrawObjCellForDummies loop /// @0x005a4ade–0x005a4b2d; LScape::draw 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 /// DrawCells entry fires before all three), RenderDeviceD3D::DrawBuilding /// @0x0059f2a0 (the part->gfxobj[deg_level]!=0 gate @0x0059f2d3 /// and the alpha-barrier → portal-pass → shell order @0x0059f30b–0x0059f345). /// 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 _events = new(); private readonly List _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 VisitedCells { get; } = new(); /// Building look-in cells in the exact order the walk encountered /// their DrawCells turns. Duplicates are intentional: two authored /// portal views can independently visit the same cell and therefore own /// distinct packed route indices. internal List LookInCellTurns { get; } = new(); private readonly List _lookInTurns = new(); private readonly List _lookInSlices = new(); private readonly List _lookInPlanes = new(); private WalkPlane _lookInCyPlane; IReadOnlyList IWalkLookInViewSource.LookInCellTurns => LookInCellTurns; /// The set form of , for drawn-once /// exclusion and root-flood particle bookkeeping. internal HashSet LookInCells { get; } = new(); /// FW4 slice 2: the interior root's ORDERED flood cell list, /// exactly as retail's PView::DrawCells iterates it for the /// exit-portal seals (pc:432785-432786) — captured at /// . 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). internal List InteriorFloodCells { get; } = new(); // Replay scratch for StaticParticles events (sequential replay — one // reused set is safe). private readonly HashSet _staticParticleOwnerScratch = new(); private static void UnionOwners( in WalkFrameStaticRecords records, HashSet destination) { foreach (RenderProjectionRecord record in records.Records) { if (record.Source.LocalEntityId != 0) destination.Add(record.Source.LocalEntityId); } } internal List VisitedBuildings { get; } = new(); internal HashSet 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); } /// /// Drives one complete frame at retail's root (SmartBox::RenderNormalMode): /// immediately followed by . Kept /// for callers that don't need the split (today: the headless referee /// suite) — RetailPViewRenderer calls the pair directly, since it /// must run other frame work BETWEEN them (plan §FW3.4a). /// 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); } /// /// Campaign FW3.4a Phase 1 — THE ONE WALK. Drives /// with this driver as its sink, /// sandwiched between /, /// performing NO GPU work: see this type's own doc comment. /// 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(); } /// /// 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 /// or /// call) and wants this /// driver's turn handling without going through the top-level root. /// is implemented in terms of this pair. Performs no /// GPU work — see this type's own doc comment. /// 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; } /// 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 — 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. internal void EndFrame() { try { MarkIfGrown(); } finally { _ctx = null; _readyToReplay = true; } } /// /// Campaign FW3.4a Phase 2. Requires a completed Collect (an /// 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 /// issues one /// WbDrawDispatcher.DrawOrderedRange call over the segment it /// closes off; every other event kind issues its corresponding /// 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. /// 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 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 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 cells) { // PView::DrawCells @0x005A4840, loop 2 (005A4A00–005A4ADE): // 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 (005A4ADE–005A4B2D): 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; } /// Campaign FW3.4a: the collect-time analogue of the old /// immediate driver's FlushIfNonEmpty — records a /// 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 , which /// hands to PrepareOrderedStream so a merge /// run can never span it — see WbDrawDispatcher.BuildOrderedMergeRuns's /// forcedBreaksAscending parameter for why that matters. 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."); /// ConstructBuildingView's polygon is building-local; the /// punch fan needs world space. Vertices transform directly; the plane /// normal uses (correct for the /// rigid, shear-free placements and /// build) and D is rederived from /// the transformed normal and the first transformed vertex — the SAME /// construction / /// already use for their own polygons (Plane = new WalkPlane(normal, /// -Vector3.Dot(normal, vertices[0]))). 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) }; } }