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, retaining authored traversal/registration /// order. For ordinary cells, builds /// the combined static/dynamic part list and performs retail's per-cell CYpt /// sort; these records are not final stream order. 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 { /// The exact combined ordinary-object membership for one indoor /// cell, preserving the registry's single static/dynamic insertion /// sequence for S4-c3a's per-part stable CYpt sort. WalkFrameStaticRecords GetCellObjects(uint cellId); /// The exact combined ordinary-object membership for one /// landscape cell; see . WalkFrameStaticRecords GetOutdoorObjects(uint cellId); /// An indoor PView::DrawCells flood cell's (or a building /// look-in's) static content — RenderProjectionClass.IndoorCellStatic. WalkFrameStaticRecords GetCellStatics(uint cellId); /// The live dynamic occupants of one indoor cell. These records /// enter the ordered stream at that cell's own object-list turn; a /// building look-in supplies its exact installed portal view for the /// per-part viewconeCheck. WalkFrameStaticRecords GetCellDynamics(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 visited landscape cell's live dynamic occupants. The /// walk draws these at the same DrawObjCell turn as its statics, /// through the landscape's currently installed view set. WalkFrameStaticRecords GetOutdoorDynamics(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(); /// S3 chunk 3 (§9.2 B1/B2), fix round 1 (F2), round 2 (F10): /// retail RenderDeviceD3D::DrawLandCell @0x0059f120 — one or more /// admitted LOD cells, possibly spanning several landblocks, submitted /// together as ONE indirect draw. /// keeps a PENDING batch that /// events append to and flushes (calls this once) immediately before any /// event whose GPU order against terrain is OBSERVABLE — see that /// method's own doc comment for the exact flush-point list and its /// pipeline-state citation. A /// is NOT one of them (F10: the ordered stream it draws is opaque-only, /// depth test AND write on, no blending); the F2 particle-turn exception /// still holds (a genuinely empty / turn neither submits nor flushes). This is /// order-preserving by construction: the batch's GPU submission point is /// always the SAME point the unbatched terrain draws would have occupied /// (immediately before the next real submission), so pixel output is /// identical to submitting each entry as its own draw. carries (landblockId, side, cellIndex) triples in /// event (draw) order — the R3 index-run arithmetic (contiguous runs /// inside each entry's own landblock's 384-index slot, /// TerrainModernRenderer.AppendCellIndexRuns) runs per entry. /// Draws UNCLIPPED for both root kinds: retail never view-clips terrain /// (LScape::draw draws whole blocks; the exit-seal/interior- /// repaint turn owns aperture exactness afterward, S3 §9.1 R4/R5). void DrawLandCellBatch( IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells); /// S3 chunk 3 fix round 1 (F2): whether has any renderable emitter right now — production /// wires this to ParticleSystem.HasRenderableEmittersInCell with /// the SAME ParticleRenderPass.Scene pass both and /// prepare through. asks this BEFORE /// flushing the pending terrain batch (or preparing either particle leaf) /// so a genuinely empty particle turn submits nothing and does not /// force an otherwise-unneeded terrain flush. bool HasRenderableEmittersInCell(uint cellId); /// 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. Retail's runtime use_built_mesh /// branch submits the complete constructed shell; the cell's drawn stamp, /// portal depth writes, and later depth-tested repaint own visibility. void DrawCellShell(uint cellId); /// One landscape (land) cell's own-cell particle emitters, at /// its own walk turn — see /// for the retail positional invariant this carries (the #132 falls /// containment). Retail CPhysicsObj::add_particle_shadow_to_cell /// (0x00514a70) draws an emitter at its own current cell's object turn /// regardless of its attached owner's registry membership, so this is a /// cell lookup, not an owner union. ReadOnlySpan PrepareStaticParticles(uint cellId); /// Submits one indoor cell's own-cell particle emitters at that /// cell's own object-list turn. Meshes have already entered the ordered /// stream before this leaf event. ReadOnlySpan PrepareCellParticles(uint cellId); /// 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). The driver calls this leaf ONLY when /// was nonzero at the /// read-then-zero decision point (S3 §8.1 R4: retail's /// portalsDrawnCount, read-then-zeroed @0x005a489c-0x005a489e — /// forceClear never writes in the pseudo-C, so the clear fires /// iff the counter was nonzero). 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) and never for a building look-in (R1: those call /// DrawCells re-entrantly with ov==0, which never reaches /// this leaf at all). void ClearInteriorDepth(); /// The exit-portal seals (pc:432785-432786) — re-stamping every /// outside-leading portal's TRUE depth right after /// (when it ran) 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. Returns the /// number of seal polygons actually submitted this turn (S3 §8.1 R4/§8.2 /// B2: retail's D3DPolyRender::portalsDrawnCount @0x008719b4 /// increments once per DrawPortalPolyInternal call with its /// second argument FALSE — exactly the exit-seal calls, never punch /// fans) — the driver adds the returned count to /// , which the NEXT /// ov>0 interior-root flood's /// decision reads. 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. int DrawExitSeals(); /// Retail D3DPolyRender::FlushAlphaList(0f) @0x005a4872 /// plus the pre-clear dynamics hook (RetailPViewRenderer's /// FlushWalkLandscape) — the first action inside /// if (outside_view.view_count > 0) at the interior root's own /// flood turn (S3 §8.1 R3), strictly before the device-stamp advance and /// the gated depth clear. Distinct from (a /// BUILDING turn's own alpha barrier, @0x0059f30b) even though both map /// to the same underlying flush call — kept as a separate leaf member so /// a driver trace can tell the two turns apart. Only called for an /// INTERIOR root's own flood with a surviving exit view (ov>0), /// never outdoors and never for a building look-in. void FlushLandscape(); /// 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); /// Retail D3DPolyRender::FlushAlphaList(0f) /// @0x0059f30b. The walk is positional: only farther cells have submitted /// alpha when a building turn fires, so this is a full drain with no /// synthetic viewer-distance threshold. void AlphaBarrier(); /// S4-c2: retail RenderDeviceD3D::DrawBlock @0x005a17c0's /// (per-land-cell loop head @0x005a18d0) own FlushAlphaList(::flush) /// @0x005a1a07 — the /// immutable global 0.75f pressure valve, inert below 2250 entries in /// either list (a no-op the overwhelming majority of the time at /// today's scene complexity) but pinned exact by the capacity/threshold /// tests. Fires once per admitted land-block cell, at the SAME turn /// records it (after that /// cell's object-list turn, before the next cell's land turn). void FlushSortCellExit(); } /// /// 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, /// Submits every retained translucent walk batch through /// up to the /// exclusive-end index in . This is a /// replay-time event: collecting the complete frame must not make alpha /// from a later/near cell visible to an earlier building barrier. AlphaSubmitMark, /// . Sky, /// — /// one admitted land cell's terrain (retail DrawLandCell /// @0x0059f120): is the landblock, /// packs side << 8 | cellIndex. /// does NOT draw it immediately: it appends the /// cell to ONE pending terrain batch that may span landblocks and /// flushes only at a flush point whose GPU order against terrain is /// OBSERVABLE — , , /// , , /// , , , /// a particle turn whose cell has a renderable emitter, or the end of /// Replay (S3 chunk 3 fix round 2, §9.7 F10; see 's /// own doc comment for the full citation). A is /// deliberately NOT a flush point: the ordered stream it draws holds /// ONLY opaque batches, depth test AND write on, no blending, so opaque /// terrain and opaque statics resolve identically in either submission /// order. A particle turn whose cell has no renderable emitter submits /// nothing and keeps the batch open (fix round 1, F2). The resolved image is therefore identical to replaying each cell /// alone: every event whose GPU order against opaque terrain is /// observable still flushes first (§9.7 F10), and opaque-vs-opaque /// order is depth-resolved. LandCell, /// — /// is the cell. Retail's /// DrawEnvCell marks the EnvCell drawn before submitting its built /// mesh, so the shell is whole and frame-deduplicated; portal-view slices /// are not shell geometry clips. 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, /// . LandscapeFlush, /// . ClearInteriorDepth, /// . ExitSeals, /// — /// ONE landscape (land) cell's () /// own-cell particle emitters, 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). A building's own shell fires /// no event of this kind any more — its interior emitters live in their /// own EnvCells and draw at those cells' own /// turns (Campaign OVERHAUL S2 chunk 6, retail /// CPhysicsObj::add_particle_shadow_to_cell 0x00514a70: an emitter /// owns one shadow in its OWN current cell, never its parent's). StaticParticles, /// — /// is the cell. Fires at EVERY /// visited interior cell's own object-list turn regardless of whether /// that cell has any visible static/dynamic owner record: an emitter /// draws at its own cell's turn independent of its attached owner's /// registry membership (a hidden/suspended owner's emitter still shows — /// Campaign OVERHAUL S2 chunk 6, the portal-haze fix). CellParticles, /// S4-c2: — /// retail RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell /// loop head @0x005a18d0) own FlushAlphaList(0.75f). SortCellExit, } /// /// 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; } /// /// Campaign OVERHAUL S3 chunk 4 (§10.2): retail's real question — does /// ANY active view slice for this DrawObjCell turn admit the part's /// drawing sphere (RenderDeviceD3D::DrawMesh @0x005A0860's /// per-view viewconeCheck loop)? Once any view admits, retail /// submits the WHOLE constructed mesh once /// (DrawMeshInternal @0x0059F360) — the portal polygon is never /// forwarded as a GPU mesh clip, so this is a pure Boolean, never a /// per-view clip-slot list (the deleted VisibleClipSlotsInLookInTurn /// always collapsed to that same Boolean one call site up anyway). /// false asks "does this route have any /// admitted view at all", ignoring the sphere geometry — retail's own /// no-authored-sphere case, where there is nothing to sphere-test. /// bool SphereVisibleInLookInTurn( int routeIndex, in Vector3 center, float radius, bool testSphere = true); /// Output-only description of the exact CY/edge-plane distances /// used by one DrawObjCell turn. Kept behind the existing Facility probe; /// production admission never consumes this text. string DescribeLookInTurn( int routeIndex, in Vector3 center, float radius) => "unavailable"; } internal readonly record struct WalkLookInSlice( int PlaneStart, int PlaneCount, uint ClipSlot); 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) { Kind = kind; IntArg = intArg; CellId = cellId; FloatArg = floatArg; Polygon = polygon; } internal WalkFrameEventKind Kind { get; } internal int IntArg { get; } internal uint CellId { get; } internal float FloatArg { get; } internal WalkPolygon? Polygon { get; } internal static WalkFrameEvent Mark(int exclusiveEnd) => new(WalkFrameEventKind.StreamMark, exclusiveEnd, 0, 0f, null); internal static WalkFrameEvent AlphaSubmitMark(int exclusiveEnd) => new(WalkFrameEventKind.AlphaSubmitMark, exclusiveEnd, 0, 0f, null); internal static WalkFrameEvent Sky() => new(WalkFrameEventKind.Sky, 0, 0, 0f, null); internal static WalkFrameEvent LandCell(uint landblockId, int sideCellCount, int cellIndex) => new(WalkFrameEventKind.LandCell, (sideCellCount << 8) | cellIndex, landblockId, 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() => new(WalkFrameEventKind.AlphaBarrier, 0, 0, 0f, null); internal static WalkFrameEvent LandscapeCellParticles( uint cellId, int alphaExclusiveEnd, bool includeParticles) => new( WalkFrameEventKind.StaticParticles, alphaExclusiveEnd, cellId, includeParticles ? 1f : 0f, null); internal static WalkFrameEvent CellParticles( uint cellId, int alphaExclusiveEnd, bool includeParticles) => new( WalkFrameEventKind.CellParticles, alphaExclusiveEnd, cellId, includeParticles ? 1f : 0f, null); internal static WalkFrameEvent LandscapeFlush() => new(WalkFrameEventKind.LandscapeFlush, 0, 0, 0f, null); internal static WalkFrameEvent ClearInteriorDepth() => new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null); internal static WalkFrameEvent ExitSeals() => new(WalkFrameEventKind.ExitSeals, 0, 0, 0f, null); internal static WalkFrameEvent SortCellExit() => new(WalkFrameEventKind.SortCellExit, 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 (, /// LandCell, CellShell, LandscapeFlush, /// 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 → flush/stamp/[gated clear]/seals /// → the flood's own cells, the middle four steps ALL gated on /// outside_view.view_count > 0 — 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)] /// [landscape flush] [gated interior depth clear] [exit-portal seals] — the /// landscape and those three only when outside_view.view_count > 0 /// (S3 §8.1 R3) — then [the interior root's own flood cells] unconditionally, /// 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 IWalkFrameLeafRenderer _leafRenderer; private readonly IWalkFrameWorldData _worldData; private readonly IWalkFrameDriverTrace? _trace; private ClipFrame? _clipFrame; private readonly OrderedDrawStream _stream = new(); private readonly List _events = new(); private readonly List _markPositions = new(); private readonly List _alphaSubmissions = new(); private int _alphaSubmitMark; // 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 readonly List _floodViewRouteScratch = new(); private WalkPlane _lookInCyPlane; /// S3 chunk 4 fix round 1 (K2): set exactly when fires this frame — i.e. a /// Landscape turn actually ran (retail's RetailFrameWalk.DrawLandscape /// only reaches that call for an outdoor root, or an interior root whose /// flood still has a surviving exit view) AND ctx.WeatherGateOpen /// held. This is "the walk's own predicate" the S3 chunk-4 fix round 1 /// unifies the transcript print and the Replay-time weather draw /// on — before this, the print's own gate (ctx.WeatherGateOpen /// alone) and the draw's gate (clipAssembly.OutsideViewSlices.Length /// > 0, re-derived independently at Replay) could diverge on an /// interior root whose landscape turn ran but whose reassembled outside- /// view slices ended up empty, letting the transcript report a weather /// turn the frame never actually drew. Reset every Collect /// (/); read at Replay /// by . internal bool WeatherTurnFired { get; private set; } /// S3 chunk 3 (§9.2 B2), fix round 1 (F2), round 2 (F10): /// 's PENDING terrain batch — every /// event since the last flush, /// across however many landblocks and however many /// events (F10: not a flush point), in event order. Cleared at the start /// of each call ("frame start") and by every flush; /// NOT cleared between events, since it is the whole point of the /// deferred-batching rule. private readonly List<(uint LandblockId, int SideCellCount, int CellIndex)> _pendingTerrainBatch = new(); private readonly HashSet _cellShellsDrawnThisFrame = new(); // Chunk 6 review F1: one particle turn per cell per render stamp (see EmitCellContentsTurn). private readonly HashSet _cellParticleTurnsDrawnThisFrame = new(); /// S3 chunk 2 (§8.2 B2): retail's D3DPolyRender::portalsDrawnCount /// (uint16 @0x008719b4) — retained ACROSS frames (this driver is itself /// retained by RetailPViewRenderer), never cleared by /// /// /// . Read-then-zeroed at every ov>0 /// interior-root flood's clear decision ( /// implementation, S3 §8.1 R4); incremented at by /// the count returns /// for THIS frame's own exit-seal turn, which the NEXT ov>0 /// frame's read will see. A driver whose interior floods never reach an /// exit portal keeps this at zero forever — the gated clear never fires /// for it, matching retail exactly (no seals submitted, no depth ever /// needed clearing). Retail consumes and re-arms inside ONE synchronous /// DrawCells; here the consume is at Collect and the re-arm at /// Replay, so a frame that Collects but never Replays (an exception path /// that already failed the frame) leaves the next ov>0 frame without a /// clear — no retail analogue, no production path. internal int PortalsDrawnCount; 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(); /// Number of exact walk-owned portal-view slices that retail's /// CEnvCell::setup_view installs while sealing one entry in /// . The matching route was captured at /// that cell's shell turn and remains valid through synchronous replay. internal int InteriorFloodViewSliceCountAt(int floodCellIndex) { int routeIndex = InteriorFloodViewRouteAt(floodCellIndex); return _lookInTurns[routeIndex].SliceCount; } /// Borrows the clip-space half-planes for one exact walk-owned /// portal-view slice. An empty span is the legitimate no-clip slot, not a /// fallback to another visibility structure. internal ReadOnlySpan InteriorFloodViewClipPlanesAt( int floodCellIndex, int sliceOffset) { int routeIndex = InteriorFloodViewRouteAt(floodCellIndex); WalkLookInTurn turn = _lookInTurns[routeIndex]; if ((uint)sliceOffset >= (uint)turn.SliceCount) throw new ArgumentOutOfRangeException(nameof(sliceOffset)); WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset]; if (_clipFrame is null) return ReadOnlySpan.Empty; return _clipFrame.GetSlotPlanes(slice.ClipSlot); } private int InteriorFloodViewRouteAt(int floodCellIndex) { if ((uint)floodCellIndex >= (uint)InteriorFloodCells.Count || (uint)floodCellIndex >= (uint)_floodViewRouteScratch.Count) { throw new ArgumentOutOfRangeException(nameof(floodCellIndex)); } int routeIndex = _floodViewRouteScratch[floodCellIndex]; if ((uint)routeIndex >= (uint)_lookInTurns.Count) { throw new InvalidOperationException( $"Interior flood cell 0x{InteriorFloodCells[floodCellIndex]:X8} " + "has no captured portal-view route."); } return routeIndex; } /// Test-only accessor (S3 chunk 4 fix round 1, K7): the packed /// GPU clip slot one look-in route's slice was assigned by 's call to — the /// SAME value resolves /// through _clipFrame.GetSlotPlanes(slice.ClipSlot) for the root- /// flood case. Restores the "a real look-in slice never reuses the /// reserved no-clip slot 0" pin the deleted VisibleClipSlotsInLookInTurn /// used to prove. indexes _lookInTurns /// directly, matching 's own /// parameter (not the flood-cell indirection goes through). internal uint LookInSliceClipSlotAt(int routeIndex, int sliceOffset = 0) { WalkLookInTurn turn = _lookInTurns[routeIndex]; if ((uint)sliceOffset >= (uint)turn.SliceCount) throw new ArgumentOutOfRangeException(nameof(sliceOffset)); return _lookInSlices[turn.SliceStart + sliceOffset].ClipSlot; } 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; /// Frame-scoped fail-loud guard (replaces the deleted /// _skyDrawnThisFrame proxy — S3 chunk 2): counts this frame's /// turns so a second one throws /// (see ) rather than double-drawing /// sky/terrain. Retail's own gate — the interior-root flood's /// landscape/flush/stamp/clear/seal turn — now reads the real /// outside_view.view_count the sink receives, not this counter. private int _landscapeTurnsThisFrame; private WalkDrawStage? _currentDcStage; private bool _readyToReplay; private int _cellViewRouteIndex; private int _landscapeViewRouteIndex; /// S3 chunk 1 (§11.2 B1): the "F n" transcript frame counter — /// monotonic across this driver's whole lifetime (never reset by /// BeginFrame/AbortFrame/EndFrame), matching a live cdb capture's own /// per-process frame counter. Only ever incremented when the transcript /// flag is on, so it costs nothing while off. private int _transcriptFrameNumber; internal WalkFrameDriver( WbDrawDispatcher dispatcher, IWalkFrameLeafRenderer leafRenderer, IWalkFrameWorldData worldData, IWalkFrameDriverTrace? trace = null, ClipFrame? clipFrame = null) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _leafRenderer = leafRenderer ?? throw new ArgumentNullException(nameof(leafRenderer)); _worldData = worldData ?? throw new ArgumentNullException(nameof(worldData)); _trace = trace; _clipFrame = clipFrame; _populator = new WalkStaticStreamPopulator(dispatcher); } /// /// FW6 allocation closeout: reuse this driver's large event/view/stream /// scratch across production frames while rebinding the frame-local leaf /// and clip destination. A prior failed frame is discarded here so the /// renderer's report-and-continue policy cannot poison the next frame. /// internal void RebindFrame( IWalkFrameLeafRenderer leafRenderer, ClipFrame? clipFrame) { AbortFrame(); _leafRenderer = leafRenderer ?? throw new ArgumentNullException(nameof(leafRenderer)); _clipFrame = clipFrame; } /// Discard only transient frame state; retained capacities stay /// available for the next frame. internal void AbortFrame() { _ctx = null; _viewProjection = default; _cameraWorldPosition = default; _landscapeTurnsThisFrame = 0; _currentDcStage = null; _readyToReplay = false; WeatherTurnFired = false; _stream.Reset(); _events.Clear(); _markPositions.Clear(); _alphaSubmissions.Clear(); _pendingTerrainBatch.Clear(); _alphaSubmitMark = 0; VisitedCells.Clear(); LookInCellTurns.Clear(); _lookInTurns.Clear(); _lookInSlices.Clear(); _lookInPlanes.Clear(); _floodViewRouteScratch.Clear(); _dispatcher.EndWalkPartFrame(); _cellShellsDrawnThisFrame.Clear(); _cellParticleTurnsDrawnThisFrame.Clear(); _lookInCyPlane = default; LookInCells.Clear(); VisitedBuildings.Clear(); VisitedLandscapeCellIds.Clear(); InteriorFloodCells.Clear(); _cellViewRouteIndex = 0; _landscapeViewRouteIndex = -1; } /// /// Copies the walk's diagnostic union: EnvCells reached by interior /// floods/look-ins plus outdoor land cells visited by the landscape walk. /// The two families remain separately retained because EnvCells are shell /// preparation input while only the landscape half feeds land-cell particle /// eligibility; EnvCell particle eligibility is the constant-true virtual. /// Point lights use the resident registry, and directional-shadow use of the /// landscape product remains owned by S5-c2. No admission path consumes this /// union as a complete CObjCell::IsInView answer. /// internal void CopyVisibleCellsTo(HashSet destination) { ArgumentNullException.ThrowIfNull(destination); if (ReferenceEquals(destination, VisitedCells) || ReferenceEquals(destination, VisitedLandscapeCellIds)) { throw new ArgumentException( "The visible-cell destination cannot alias a walk source set.", nameof(destination)); } destination.Clear(); destination.UnionWith(VisitedCells); destination.UnionWith(VisitedLandscapeCellIds); } /// /// 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); // S3 chunk 1 (§11.2 B1): the frame-root "F n" + "P …" print — AFTER // BeginFrame (a scratch reset, not an admission decision), BEFORE // the walk itself runs, matching retail's SmartBox::RenderNormalMode // @0x00453aa0 breakpoint (entered once per frame, before either the // outdoor or the indoor branch). Every cost here (the counter // increment, the landblock-local subtraction) sits inside the flag // check, so an ordinary launch pays nothing. if (AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled) { _transcriptFrameNumber++; WalkTranscriptDump.PrintFrameRoot( _transcriptFrameNumber, cameraCellId, cameraWorldPosition - new Vector3( landscape.ViewerWorldOriginX, landscape.ViewerWorldOriginY, 0f), ctx.CyPlane.Normal); } try { walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this); EndFrame(); } catch { AbortFrame(); throw; } } /// /// 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."); } _dispatcher.BeginWalkPartFrame(); _ctx = ctx; _viewProjection = viewProjection; _cameraWorldPosition = cameraWorldPosition; _landscapeTurnsThisFrame = 0; _currentDcStage = null; _readyToReplay = false; WeatherTurnFired = false; _stream.Reset(); _events.Clear(); _markPositions.Clear(); _alphaSubmissions.Clear(); _pendingTerrainBatch.Clear(); _alphaSubmitMark = 0; VisitedCells.Clear(); LookInCellTurns.Clear(); _lookInTurns.Clear(); _lookInSlices.Clear(); _lookInPlanes.Clear(); _lookInCyPlane = ctx.CyPlane; _cellShellsDrawnThisFrame.Clear(); _cellParticleTurnsDrawnThisFrame.Clear(); LookInCells.Clear(); VisitedBuildings.Clear(); VisitedLandscapeCellIds.Clear(); InteriorFloodCells.Clear(); _cellViewRouteIndex = 0; _landscapeViewRouteIndex = -1; } /// 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(); MarkAlphaIfGrown(); } finally { _dispatcher.EndWalkPartFrame(); _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. /// /// S3 chunk 3 fix round 2 (§9.7 F10) — the pending terrain /// batch's flush points, authoritative list. A /// is NOT one of them: the /// ordered stream it draws holds ONLY opaque batches — WalkStaticStreamPopulator's /// per-entity classify step appends to the stream iff batch.IsOpaque /// and otherwise routes translucent batches to the alpha list /// (WalkStaticStreamPopulator.cs:179) — drawn through the Opaque/ /// OpaqueAlphaToCoverage pipelines, both built with depth test AND /// write ON and NO blending (WbDrawDispatcher.CreateMeshPipeline: /// Depth = new GpuDepthState(Test: true, Write: depthWrite, /// depthCompare) called with depthWrite: true, /// GpuBlendMode.None for both, WbDrawDispatcher.Rhi.cs:240-248,343); /// DrawOrderedRange resolves every walk-stream command's bucket to /// PipelineBucket.Opaque because every command it holds is opaque /// (WbDrawDispatcher.OrderedStream.cs:569-571). Opaque terrain and opaque /// statics are therefore depth-resolved correctly in EITHER submission /// order — pixel-identical except exact z-ties — so deferring the pending /// terrain batch across a StreamMark changes nothing observable. /// The flush points that DO remain, because their GPU order against /// terrain IS observable: /// (DEPTHTEST_ALWAYS + write — the far-Z stamp this chunk's /// interleave exists to order correctly against nearer terrain, S3 §9.1 /// R4), and /// (both drain the /// translucent alpha list, which must see every opaque surface — terrain /// included — already resolved in the depth buffer), /// , /// , /// , , /// a / /// turn whose cell has a /// renderable emitter (a genuinely empty turn still submits nothing and /// still does not flush, unchanged from fix round 1's F2), and the end of /// this method. This list's premise is the opaque-only, depth-test-and- /// write, no-blend pipeline pair the walk's ordered stream actually uses /// TODAY — not StreamMark itself: if any future ordered-stream /// stage draws with blending or a disabled depth write (for example the /// deferred building-detail overlay replay, /// WbDrawDispatcher.DrawBuildingDetailRangeRhi's /// RetailDetailTransparent pass — not wired into this walk path /// today, OrderedStream.cs:46-52), that stage's StreamMark MUST /// become a flush point again; the contract gets amended, not bent. /// 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."); } try { if (_stream.Count > 0) _dispatcher.PrepareOrderedStream(frame, _stream, _viewProjection, _markPositions); // S3 chunk 3 fix round 1 (F2): cleared at the start of THIS // replay ("frame start") — a batch never survives across frames. _pendingTerrainBatch.Clear(); int cursor = 0; int alphaCursor = 0; for (int i = 0; i < _events.Count; i++) { WalkFrameEvent e = _events[i]; switch (e.Kind) { case WalkFrameEventKind.StreamMark: // S3 chunk 3 fix round 2 (F10): NOT a terrain flush // point. The ordered stream this draws holds ONLY // opaque batches (WalkStaticStreamPopulator.cs:179) // drawn with the Opaque/OpaqueAlphaToCoverage // pipelines — depth test AND write on, no blending // (WbDrawDispatcher.Rhi.cs:240-248,343; bucket // resolution WbDrawDispatcher.OrderedStream.cs:569-571) // — so opaque terrain and opaque statics are // depth-resolved correctly in either submission // order. See this method's own doc comment for the // full flush-point list and the citation this rests // on. 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.AlphaSubmitMark: // Not a flush point (unchanged by fix round 2, F10): // this arm only ENQUEUES the translucent instances // into the CPU alpha list (SubmitWalkAlphaInstance); // nothing reaches the GPU until the list drains at an // AlphaBarrier / LandscapeFlush leaf or the pass end // — both remain terrain flush points (F10) — so the // drained alpha still lands after every preceding // land cell's terrain, exactly as when unbatched. int alphaEnd = e.IntArg; for (; alphaCursor < alphaEnd; alphaCursor++) { WbDrawDispatcher.WalkClassifiedBatch batch = _alphaSubmissions[alphaCursor]; _dispatcher.SubmitWalkAlphaInstance( in batch, _viewProjection); } break; case WalkFrameEventKind.Sky: FlushPendingTerrainBatch(); _leafRenderer.DrawSky(); break; case WalkFrameEventKind.LandCell: // S3 chunk 3 fix round 1 (F2), round 2 (F10): // append-only — no lookahead, no same-landblock // restriction, no flush. The batch may span several // landblocks; it flushes only at one of the F10 // flush points (PunchFan, AlphaBarrier, // LandscapeFlush, ClearInteriorDepth, ExitSeals, // Sky, CellShell, a has-emitter particle turn) or at // Replay's own end — NOT at a StreamMark, since the // opaque ordered stream's GPU order against terrain // is unobservable (see Replay's own doc comment). _pendingTerrainBatch.Add((e.CellId, e.IntArg >> 8, e.IntArg & 0xFF)); break; case WalkFrameEventKind.CellShell: FlushPendingTerrainBatch(); _leafRenderer.DrawCellShell(e.CellId); break; case WalkFrameEventKind.PunchFan: FlushPendingTerrainBatch(); _leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg); break; case WalkFrameEventKind.AlphaBarrier: FlushPendingTerrainBatch(); _leafRenderer.AlphaBarrier(); break; case WalkFrameEventKind.SortCellExit: FlushPendingTerrainBatch(); _leafRenderer.FlushSortCellExit(); break; case WalkFrameEventKind.LandscapeFlush: FlushPendingTerrainBatch(); _leafRenderer.FlushLandscape(); break; case WalkFrameEventKind.ClearInteriorDepth: FlushPendingTerrainBatch(); _leafRenderer.ClearInteriorDepth(); break; case WalkFrameEventKind.ExitSeals: FlushPendingTerrainBatch(); // S3 §8.2 B2: the driver — not Collect — owns adding // the leaf's returned submitted-fan count to the // persistent PortalsDrawnCount, since the real // enumeration (and therefore the real count) only // exists once the production leaf actually runs, at // Replay. PortalsDrawnCount += _leafRenderer.DrawExitSeals(); break; case WalkFrameEventKind.StaticParticles: // S3 chunk 3 fix round 1 (F2): a genuinely empty // particle turn submits nothing — and, since it // submits nothing, must NOT flush the pending // terrain batch either (the batch is order-preserving // only because a skipped turn changes nothing about // what reaches the GPU, at all). Retail // CPhysicsObj::add_particle_shadow_to_cell // (0x00514a70): an emitter owns one shadow in its OWN // current cell, drawn at that cell's object turn // regardless of any owner's registry membership — // this is a cell lookup, not an owner union. SubmitCellAlpha( e, ref alphaCursor, staticParticleTurn: true); break; case WalkFrameEventKind.CellParticles: SubmitCellAlpha( e, ref alphaCursor, staticParticleTurn: false); break; } } // S3 chunk 3 fix round 1 (F2), round 2 (F10): the end of Replay // is itself a flush point — the remainder never crosses into // the next frame. FlushPendingTerrainBatch(); } finally { _stream.Reset(); _events.Clear(); _markPositions.Clear(); _alphaSubmissions.Clear(); _pendingTerrainBatch.Clear(); _alphaSubmitMark = 0; _readyToReplay = false; _ctx = null; } } /// /// S4-c3a: completes one retail object-cell turn. Object delayed subsets /// are already in stable far→near part order. Real particle records are /// prepared only now, then merged by their retained CYpt before either /// source appends to the two untouched FIFO queue lists. Object wins an /// exactly equal cross-source key; AP-242 records that deterministic /// source tie because acdream has no shared physical registration /// ordinal. The event always drains its object range, including a cell /// with no particle emitters. /// private void SubmitCellAlpha( in WalkFrameEvent e, ref int alphaCursor, bool staticParticleTurn) { ReadOnlySpan particles = ReadOnlySpan.Empty; bool includeParticles = e.FloatArg != 0f; if (includeParticles && _leafRenderer.HasRenderableEmittersInCell(e.CellId)) { // Preserve the existing particle-turn terrain boundary: a row-5 // immediate mesh may draw during preparation. FlushPendingTerrainBatch(); particles = staticParticleTurn ? _leafRenderer.PrepareStaticParticles(e.CellId) : _leafRenderer.PrepareCellParticles(e.CellId); } int objectEnd = e.IntArg; int particleIndex = 0; while (alphaCursor < objectEnd || particleIndex < particles.Length) { bool takeParticle = particleIndex < particles.Length && (alphaCursor >= objectEnd || particles[particleIndex].DistanceSq > _alphaSubmissions[alphaCursor].SortDistanceSq); if (takeParticle) { particles[particleIndex++].Append(); } else { WbDrawDispatcher.WalkClassifiedBatch batch = _alphaSubmissions[alphaCursor++]; _dispatcher.SubmitWalkAlphaInstance(in batch, _viewProjection); } } } /// S3 chunk 3 fix round 1 (F2), round 2 (F10): submits the /// pending terrain batch as ONE /// call, if it holds anything, and clears it. Called immediately before /// every event kind whose GPU order against terrain is OBSERVABLE — see /// 's own doc comment for the full flush-point list /// and its citation — so the batch's actual submission point is always /// the exact point the unbatched per-cell draws would have occupied /// relative to those events. A /// is deliberately NOT one of the callers (F10): the opaque ordered /// stream it draws is depth-resolved identically regardless of /// submission order relative to terrain. private void FlushPendingTerrainBatch() { if (_pendingTerrainBatch.Count == 0) return; _leafRenderer.DrawLandCellBatch(_pendingTerrainBatch); _pendingTerrainBatch.Clear(); } // ------------------------------------------------------------------ // IWalkEventSink // ------------------------------------------------------------------ void IWalkEventSink.Emit(in WalkEvent walkEvent) { switch (walkEvent.Kind) { case WalkEventKind.DrawInside: // S3 chunk 1 (§11.2 B1): PView::DrawInside @0x005a5860 entry. WalkTranscriptDump.PrintDrawInside(walkEvent.CellId); _currentDcStage = WalkDrawStage.CellStatic; VisitedCells.Add(walkEvent.CellId); break; case WalkEventKind.Landscape: // S3 chunk 1 (§11.2 B1): LScape::draw @0x00506330 entry — // printed before the driver's own duplicate-turn guard so a // desynced second LS still shows up in the transcript // (the guard is a walk/driver fail-loud check, not a retail // admission decision retail's own breakpoint would skip). WalkTranscriptDump.PrintLandscape(); HandleLandscapeTurn(walkEvent.OutsideViewCount); break; case WalkEventKind.DrawCells: // S3 chunk 1 (§11.2 B1): PView::DrawCells @0x005a4840 entry // (breakpoint-entry order, matching the FW0/OH captures). // _currentDcStage at THIS point (set by the immediately // preceding DI or OnBuildingTurn event, never mutated by // this case itself) tells us which PView owns this flood: // CellStatic == the interior root's own flood (interior // pview); anything else (LookInStatic) == a building // look-in through the outdoor pview. WalkTranscriptDump.PrintDrawCells( outdoorPview: _currentDcStage == WalkDrawStage.LookInStatic, walkEvent.OutsideViewCount, walkEvent.Cells); foreach (uint id in walkEvent.Cells) VisitedCells.Add(id); HandleDrawCellsTurn(walkEvent.Cells); break; case WalkEventKind.Building: // S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawBuilding // @0x0059f2a0 entry — unconditional, matching the BLD event's // own "fires at ENTRY, before the degrade check" contract // (this type's class doc). WalkTranscriptDump.PrintBuilding(walkEvent.CellId); // OnBuildingTurn (below) carries the actual side effects — // this vocabulary-only event needs no driver action. break; } } /// S3 chunk 3 (§9.2 B1): records this admitted land cell's /// terrain turn. sideCellCount/cellIndex travel packed /// (not expanded) — R3's index-run arithmetic runs at LOD resolution in /// the production leaf, not at 8×8-bucket resolution like the object- /// list turn's expansion. void IWalkEventSink.OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex) { RequireOpenFrame(); if (sideCellCount is not (1 or 2 or 4 or 8)) { throw new ArgumentOutOfRangeException( nameof(sideCellCount), sideCellCount, "A landscape LOD grid must be 1, 2, 4, or 8 cells per side."); } if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount)) throw new ArgumentOutOfRangeException(nameof(cellIndex)); // S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawLandCell @0x0059f120 // entry — one LC line per admitted LOD land cell, at LOD resolution // (never expanded to the 8x8 owner-bucket ids OnLandscapeCellTurn // uses for object-list content). S3 chunk 1 fix round 1 (G2): the // flag gate wraps the LodCellId computation itself, not just // WalkTranscriptDump.PrintLandCell's own internal check — an // ordinary launch (flag off) must not pay for the division/modulo/ // checked-conversion work on ~1,300 land-cell turns/frame just to // discard the result. if (AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled) { WalkTranscriptDump.PrintLandCell( WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex)); } // The one mark rule (this type's own doc comment): flush any // already-queued stream content ahead of this leaf-renderer event. MarkIfGrown(); _events.Add(WalkFrameEvent.LandCell(landblockId, sideCellCount, cellIndex)); } /// S3 chunk 1 fix round 1 (G7): RenderDeviceD3D:: /// DrawSortCell @0x0059f140 ENTRY — fired by /// BEFORE that turn's own DrawBuilding call, so the SC line /// prints between LC and BLD (retail's own order — see the interface /// member's own doc comment; the three-lens review's transcript pass /// caught the earlier placement, which printed SC from /// AFTER /// DrawBuilding had already run). Print-only — no stream-side effect of /// its own; the object-list turn stays entirely in /// OnLandscapeCellTurn below, unchanged. /// /// S3 chunk 1 fix round 2 (§11.6 H4): UNLIKE /// (whose RequireOpenFrame/range validation stay unconditional — /// that hook always records a real WalkFrameEvent), this hook has /// NO side effect beyond the print, so the flag gate wraps the ENTIRE /// body, not just the LodCellId computation: an ordinary launch /// pays exactly one interface dispatch per visited land cell and nothing /// else (docs/launch-options.md). /// /// void IWalkEventSink.OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex) { if (!AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled) return; RequireOpenFrame(); if (sideCellCount is not (1 or 2 or 4 or 8)) { throw new ArgumentOutOfRangeException( nameof(sideCellCount), sideCellCount, "A landscape LOD grid must be 1, 2, 4, or 8 cells per side."); } if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount)) throw new ArgumentOutOfRangeException(nameof(cellIndex)); WalkTranscriptDump.PrintSortCell( WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex)); } void IWalkEventSink.OnLandscapeCellTurn(uint cellId) => HandleLandscapeCellTurn(cellId); /// The object-list half of retail's DrawSortCell turn — see /// for the SC transcript /// line, which now fires separately, BEFORE the building's own turn /// (S3 chunk 1 fix round 1, G7); this method never prints one. void IWalkEventSink.OnLandscapeCellTurn( uint landblockId, int sideCellCount, int cellIndex) { if (sideCellCount is not (1 or 2 or 4 or 8)) { throw new ArgumentOutOfRangeException( nameof(sideCellCount), sideCellCount, "A landscape LOD grid must be 1, 2, 4, or 8 cells per side."); } if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount)) throw new ArgumentOutOfRangeException(nameof(cellIndex)); uint blockPrefix = landblockId & 0xFFFF0000u; if (sideCellCount == 8) { HandleLandscapeCellTurn(blockPrefix | checked((uint)(cellIndex + 1))); return; } // CLandBlock's coarse DrawSortCell owns the shadows from every // original 24 m land cell it covers. Our render journal deliberately // retains those authoritative 8x8 owner buckets, so turn all covered // buckets here while preserving the coarse cell's single walk turn. int span = 8 / sideCellCount; int coarseX = cellIndex / sideCellCount; int coarseY = cellIndex % sideCellCount; int firstX = coarseX * span; int firstY = coarseY * span; for (int x = firstX; x < firstX + span; x++) { for (int y = firstY; y < firstY + span; y++) { HandleLandscapeCellTurn( blockPrefix | checked((uint)(x * 8 + y + 1))); } } } private void HandleLandscapeCellTurn(uint cellId) { RequireOpenFrame(); if (_landscapeViewRouteIndex < 0) { throw new InvalidOperationException( "A landscape cell turn fired before RetailFrameWalk installed its active " + "view set — the walk and draw driver are desynchronized."); } VisitedLandscapeCellIds.Add(cellId); WalkFrameStaticRecords records = _worldData.GetOutdoorObjects(cellId); _populator.PopulateCellObjects( _stream, WalkDrawStage.OutdoorStatic, cellId, records.Records, records.TupleLandblockId, _cameraWorldPosition, _viewProjection, this, _landscapeViewRouteIndex, _alphaSubmissions); // FW4 (the #132 positional invariant): this cell's emitters 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). Fires // unconditionally: retail's add_particle_shadow_to_cell draws an // emitter at its own cell's turn independent of whether that cell // has any visible static/dynamic owner record (Campaign OVERHAUL S2 // chunk 6 — a suspended/hidden owner's emitter must still show). MarkIfGrown(); bool includeParticles = _cellParticleTurnsDrawnThisFrame.Add(cellId); _alphaSubmitMark = _alphaSubmissions.Count; _events.Add(WalkFrameEvent.LandscapeCellParticles( cellId, _alphaSubmitMark, includeParticles)); } /// S4-c2: retail RenderDeviceD3D::DrawBlock @0x005a17c0's /// (per-land-cell loop head @0x005a18d0) own FlushAlphaList(::flush) /// @0x005a1a07 (the 0.75f /// pressure valve) — fires once per admitted land-block cell, at the SAME /// gate /// already fires under (RetailFrameWalk.DrawLandscape calls this /// immediately after that turn, still inside the loop body). Marks first, /// same as 's AlphaBarrier /// emission, so this cell's own content flushes ahead of the valve. void IWalkEventSink.OnSortCellExit(uint landblockId, int sideCellCount, int cellIndex) { RequireOpenFrame(); MarkIfGrown(); MarkAlphaIfGrown(); _events.Add(WalkFrameEvent.SortCellExit()); } void IWalkEventSink.OnLandscapeViews(WalkPortalView activeViews) { ArgumentNullException.ThrowIfNull(activeViews); RequireOpenFrame(); _landscapeViewRouteIndex = _cellViewRouteIndex++; CaptureViews(0, activeViews); } void IWalkEventSink.OnBuildingTurn(WalkBuilding building) { ArgumentNullException.ThrowIfNull(building); 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(); MarkAlphaIfGrown(); _events.Add(WalkFrameEvent.AlphaBarrier()); _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, alphaSubmissions: _alphaSubmissions); if (_alphaSubmissions.Count != _alphaSubmitMark) { MarkIfGrown(); MarkAlphaIfGrown(); } // Campaign OVERHAUL S2 chunk 6: a building's shell fires no particle // turn of its own any more. Retail's add_particle_shadow_to_cell // (0x00514a70) gives an emitter exactly one shadow in ITS OWN // current cell — a building's interior emitters live in their own // EnvCells and draw at those cells' own CellParticles turns, never // at their parent building's shell turn. } void IWalkEventSink.OnPunchGeometry( WalkBuilding building, WalkPolygon polygon, int activeViewIndex) { ArgumentNullException.ThrowIfNull(building); ArgumentNullException.ThrowIfNull(polygon); RequireOpenFrame(); // S4-c1 C1: DrawPortalPolyInternal's degenerate-input guard // (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard's own // doc comment) — tested on the polygon's LOCAL vertices, BEFORE the // building -> world transform below. A hit drops the whole polygon: // no punch event, no transform, no counter effect (retail's own // reject -> transform -> clip -> count order). if (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon.Vertices)) return; MarkIfGrown(); Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building); _events.Add( WalkFrameEvent.PunchFan(TransformToWorld(polygon, worldTransform), activeViewIndex)); } void IWalkEventSink.OnInteriorFloodDrawTurn(IReadOnlyList cells, int outsideViewCount) { ArgumentNullException.ThrowIfNull(cells); RequireOpenFrame(); // PView::DrawCells @0x005a4840 (S3 §8.1 R3): the landscape flush, the // device-stamp advance, the gated depth clear, and the exit-portal // seals ALL sit strictly inside `if (outside_view.view_count > 0)` // (0x005a4852-0x005a49eb) — outsideViewCount==0 skips straight to // the flood's own cells below with none of the four having run. if (outsideViewCount > 0) { // D3DPolyRender::FlushAlphaList(0f) @0x005a4872, plus the // pre-clear dynamics hook RetailPViewRenderer.FlushWalkLandscape // bundles — the first action inside the gate. MarkIfGrown(); _events.Add(WalkFrameEvent.LandscapeFlush()); // m_nFrameStamp += 1 @0x005a4886. Its drawn-part AND DrawEnvCell // dedupe is per render stamp, not per presented frame: content // admitted during the landscape must remain eligible for the // interior-cell repaint after the clear. In Collect, every // landscape candidate has been classified by this point and no // interior-root candidate has, so re-arming both CPU-side stamp // mirrors here is the exact boundary. Without the shell re-arm, // color from a pre-clear building look-in survives while the // root repaint is incorrectly suppressed, producing // wall-textured bleed slabs. _dispatcher.AdvanceWalkPartPassStamp(); _cellShellsDrawnThisFrame.Clear(); _cellParticleTurnsDrawnThisFrame.Clear(); // The gated full depth clear (pc:432731-432732): retail's // portalsDrawnCount (D3DPolyRender::portalsDrawnCount, // uint16 @0x008719b4) is read-then-zeroed HERE // (@0x005a489c-0x005a489e; R4) — forceClear never writes in the // pseudo-C, so the clear fires iff the counter was nonzero. A // fresh driver's very first ov>0 frame therefore draws NO clear // (portalsDrawnCount starts at 0); every later ov>0 frame clears // because the PREVIOUS frame's own exit seals armed the counter // at Replay (see PortalsDrawnCount's own doc comment). int armed = PortalsDrawnCount; PortalsDrawnCount = 0; if (armed != 0) { MarkIfGrown(); _events.Add(WalkFrameEvent.ClearInteriorDepth()); } // The exit-portal seals (pc:432785-432786) — this turn's // submitted fan count re-arms PortalsDrawnCount at Replay (B2), // read by the NEXT ov>0 frame's decision above. 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); } /// S3 chunk 4 (O3): retail's GameSky::Draw(sky,1) /// @0x00506ff0 weather pass — /// only calls this when its own gate already held, so this handler /// records unconditionally (S3 chunk 4 /// fix round 1, K2 — the one predicate the Replay-time draw now reads, /// so a landscape turn that ran with the weather gate open can never be /// under-reported to the draw side) and is otherwise print-only, /// matching 's own shape (no /// stream side effect). The GPU draw stays where chunk 1 round 2 put /// it — , called /// directly from /// at Replay time, now gated on THIS flag instead of re-deriving its /// own independent condition. void IWalkEventSink.OnWeatherTurn(uint viewerCellId) { RequireOpenFrame(); WeatherTurnFired = true; if (!AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled) return; WalkTranscriptDump.PrintObjectCellTurn(viewerCellId); } // ------------------------------------------------------------------ // 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 (_landscapeTurnsThisFrame != 0) { 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()); _landscapeTurnsThisFrame++; // S3 chunk 3 (§9.1 R4/§9.2 B1): the FW4-slice-6 whole-stage terrain // turn (retail draws terrain blocks ONCE per landscape turn) is // SUPERSEDED — retail's true per-cell DrawLandCell/DrawSortCell // interleave now emits one WalkFrameEventKind.LandCell turn per // admitted land cell (IWalkEventSink.OnLandCellTurn, fired from // RetailFrameWalk.DrawLandscape's own per-cell loop), not a single // TerrainSlice(0) here. S3 chunk 3 fix round 1 (F6) corrects the // direction of this note: drawing ALL terrain BEFORE every building // let a FARTHER building's far-Z punch survive, because the NEARER // terrain that retail draws AFTER that punch (and which overwrites // it) was instead drawn BEFORE it here — nothing later covered the // punched depth, so a doorway behind a hill punched straight through // it. The interleave fixes this by ORDER alone: nearer terrain now // draws AFTER the farther building's punch, exactly like retail. The // walk's active views still own only the punch fans and dynamics // apertures; terrain itself is UNCLIPPED for both root kinds. } 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) { _floodViewRouteScratch.Clear(); for (int i = 0; i < cells.Count; i++) _floodViewRouteScratch.Add(-1); // PView::DrawCells @0x005A4840, loop 2 (005A4A00–005A4ADE): // cell_draw_list[count - 1] down to zero. The loop calls setup_view + // DrawEnvCell for each live view, but DrawEnvCell @0x0059F170 begins // with GetDrawnThisFrame/SetDrawnThisFrame. Its built-mesh path then // calls D3DPolyRender::DrawMesh directly: the EnvCell shell is drawn // WHOLE on the first call and every later view/cross-flood call is a // no-op for this render stamp. Portal-view clipping remains active for // the following object-list loop; it is not a shell geometry clip. for (int i = cells.Count - 1; i >= 0; i--) { int viewRouteIndex = CaptureCellViewRoute(cells[i]); _floodViewRouteScratch[i] = viewRouteIndex; // S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawEnvCell // @0x0059f170 entry — UNCONDITIONAL, before the per-render-stamp // dedupe below. Retail's own stamp check (GetDrawnThisFrame/ // SetDrawnThisFrame) sits INSIDE DrawEnvCell, past this // breakpoint's address, so every flood visit prints an EC line // even when the visit's actual shell submission is suppressed — // proof: holtburg-doorway-still.walk.log:1126,1131,1134,1137 // print "EC a9b40100" four times in ONE frame across four // look-in DC turns that all redraw the same cell. S3 chunk 1 fix // round 2 (§11.6 H2): "EC and OC counts are always exactly // equal" (an earlier version of this comment) is FALSE — // terrace-edge has 12 EC vs 16 OC, the extra 4 being the // trailing per-frame weather OC (§11.6 H1), which has no EC // counterpart at all; see WalkTranscriptDump.PrintEnvCellShell's // own doc comment for the same citation. // // S3 review fix round 1 (F4a): retail's breakpoint sits inside // the setup_view/DrawEnvCell LOOP (PView::DrawCells // @0x005a4ab1-0x005a4acc), which iterates the cell's live // portal_view slices — @0x0059f170's own entry, above, fires // once PER live view, not once per cell. Read the SliceCount // directly off the route CaptureCellViewRoute just captured // (_lookInTurns[viewRouteIndex]) rather than through // InteriorFloodViewSliceCountAt's flood-index indirection: this // loop also runs for a building's LOOK-IN flood // (WalkDrawStage.LookInStatic), whose cells never populate // InteriorFloodCells (that list is the interior ROOT flood // only — see its own doc comment) — indexing it here would // throw ArgumentOutOfRangeException on a real look-in turn. A // cell with zero live views (an untracked/degenerate route) // prints zero EC lines, matching setup_view never iterating an // empty view list. The SHELL DRAW event below is unchanged — // the frame stamp still dedupes it to one submission regardless // of how many times this prints. // Guarded at the call site (the §11.5 G2 convention): flag off // costs one bool read here, not a list index plus N no-op calls. if (AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled) { int liveViewCount = _lookInTurns[viewRouteIndex].SliceCount; for (int view = 0; view < liveViewCount; view++) WalkTranscriptDump.PrintEnvCellShell(cells[i]); } if (_cellShellsDrawnThisFrame.Add(cells[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], _floodViewRouteScratch[i]); } private int CaptureCellViewRoute(uint cellId) { int viewRouteIndex = _cellViewRouteIndex++; CaptureCellViews(cellId); return viewRouteIndex; } private void EmitCellContentsTurn( WalkDrawStage stage, uint cellId, int viewRouteIndex) { // S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawObjCellForDummies // @0x005a0760 entry — the interior/look-in object-list turn (retail's // outdoor DrawObjCell is a different function, folded into the SC // line's own DrawSortCell call). Unconditional, matching this loop's // existing no-dedupe shape (every flood visit already calls this // method once, with no per-stamp gate). WalkTranscriptDump.PrintObjectCellTurn(cellId); // The shell loop captured the exact live portal_view for this cell. // Static and dynamic object lists reuse that same route: retail's // DrawEnvCell and DrawObjCell consume the same PortalList. WalkFrameStaticRecords records = _worldData.GetCellObjects(cellId); _populator.PopulateCellObjects( _stream, stage, cellId, records.Records, records.TupleLandblockId, _cameraWorldPosition, _viewProjection, this, viewRouteIndex, _alphaSubmissions); MarkIfGrown(); if (stage == WalkDrawStage.LookInStatic) { LookInCellTurns.Add(cellId); LookInCells.Add(cellId); } // Fires for every cell turn regardless of whether that cell has any // visible static/dynamic owner record (Campaign OVERHAUL S2 chunk 6 — // a suspended/hidden owner's emitter must still show), but ONCE per // render stamp: retail's particle parts sit in the same shadow_part_list // as every other part, so CPhysicsPart::Draw's frame stamp suppresses a // second submission when one cell gets two object-list turns in one // frame (a chamber reached through two portals). The same frame-scoped // set that dedupes the cell SHELL dedupes the particle turn (chunk 6 // review F1). bool includeParticles = _cellParticleTurnsDrawnThisFrame.Add(cellId); _alphaSubmitMark = _alphaSubmissions.Count; _events.Add(WalkFrameEvent.CellParticles( cellId, _alphaSubmitMark, includeParticles)); } private void CaptureCellViews(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; } CaptureViews(cellId, cell.TopView); } private void CaptureViews(uint cellId, WalkPortalView portalView) { 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); } uint clipSlot = AppendClipSlot(portalView, poly); _lookInSlices.Add(new WalkLookInSlice( planeStart, poly.VertexCount, clipSlot)); } _lookInTurns.Add(new WalkLookInTurn( cellId, sliceStart, _lookInSlices.Count - sliceStart)); } /// See the interface member's own doc comment (S3 chunk 4, /// §10.2): replaces the deleted VisibleClipSlotsInLookInTurn's /// per-slice loop with the SAME admission condition per slice, short- /// circuiting to a Boolean the moment any slice admits instead of /// accumulating a slot list nothing downstream read for its content /// (every caller only ever asked Count == 0). public bool SphereVisibleInLookInTurn( int routeIndex, in Vector3 center, float radius, bool testSphere = true) { 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 (!testSphere || WalkVisibilityMath.ViewconeCheck( center, radius, _lookInCyPlane, CollectionsMarshal.AsSpan(_lookInPlanes).Slice( slice.PlaneStart, slice.PlaneCount)) != WalkBoundingType.Outside) { return true; } } return false; } public string DescribeLookInTurn( int routeIndex, in Vector3 center, float radius) { if ((uint)routeIndex >= (uint)_lookInTurns.Count) return "route-missing"; WalkLookInTurn turn = _lookInTurns[routeIndex]; var description = new System.Text.StringBuilder(192); float cyDistance = Vector3.Dot(_lookInCyPlane.Normal, center) + _lookInCyPlane.D; description.Append("turnCell=0x") .Append(turn.CellId.ToString("X8")) .Append(" slices=").Append(turn.SliceCount) .Append(" cy=").Append(cyDistance.ToString("F5")) .Append(" cyMargin=").Append((cyDistance + radius).ToString("F5")); for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++) { WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset]; description.Append(" slice[").Append(sliceOffset).Append("]="); for (int planeOffset = 0; planeOffset < slice.PlaneCount; planeOffset++) { if (planeOffset != 0) description.Append(','); WalkPlane plane = _lookInPlanes[slice.PlaneStart + planeOffset]; float distance = Vector3.Dot(plane.Normal, center) + plane.D; description.Append(distance.ToString("F5")); } } return description.ToString(); } /// Converts one retail pixel-space portal_view polygon into the /// clip-space half-planes consumed by mesh_modern.vert. copy_view already /// deduplicates and removes collinear vertices, so 3..8 points map /// directly. The rare >8-edge case uses its convex AABB as a conservative /// four-plane gate rather than falling back to slot 0 (which would erase /// the aperture entirely). private uint AppendClipSlot(WalkPortalView portalView, WalkViewPoly poly) { if (_clipFrame is null) return 0; IRetailFrameWalkContext ctx = (IRetailFrameWalkContext)RequireOpenFrame(); int count = poly.VertexCount; if (count < 3) return 0; Span ndc = stackalloc Vector2[Math.Min(count, WalkCopyView.MaxVertices)]; float minX = float.MaxValue, minY = float.MaxValue; float maxX = float.MinValue, maxY = float.MinValue; for (int i = 0; i < ndc.Length; i++) { Vector2 px = portalView.View.Vertices[poly.VertexIndex + i].Point; Vector2 point = new( px.X / ctx.ViewportWidth * 2f - 1f, 1f - px.Y / ctx.ViewportHeight * 2f); ndc[i] = point; minX = MathF.Min(minX, point.X); minY = MathF.Min(minY, point.Y); maxX = MathF.Max(maxX, point.X); maxY = MathF.Max(maxY, point.Y); } if (ndc.Length > ClipFrame.MaxPlanes) { Span aabbPlanes = stackalloc Vector4[4] { new(1f, 0f, 0f, -minX), new(-1f, 0f, 0f, maxX), new(0f, 1f, 0f, -minY), new(0f, -1f, 0f, maxY), }; return checked((uint)_clipFrame.AppendSlot(aabbPlanes)); } float area2 = 0f; for (int i = 0; i < ndc.Length; i++) area2 += ndc[i].X * ndc[(i + 1) % ndc.Length].Y - ndc[(i + 1) % ndc.Length].X * ndc[i].Y; bool ccw = area2 >= 0f; Span planes = stackalloc Vector4[ClipFrame.MaxPlanes]; for (int i = 0; i < ndc.Length; i++) { int current = ccw ? i : ndc.Length - 1 - i; int next = ccw ? (i + 1) % ndc.Length : (ndc.Length - 2 - i + ndc.Length) % ndc.Length; Vector2 p = ndc[current]; Vector2 q = ndc[next]; Vector2 dir = q - p; Vector2 normal = Vector2.Normalize(new Vector2(-dir.Y, dir.X)); planes[i] = new Vector4(normal.X, normal.Y, 0f, -Vector2.Dot(normal, p)); } return checked((uint)_clipFrame.AppendSlot(planes[..ndc.Length])); } /// 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)); } /// Records translucent walk batches at the same temporal point /// as their cell turn. Replay submits them only when it reaches this /// marker, so an earlier building's full alpha barrier cannot see content /// that the far-to-near walk has not reached yet. private void MarkAlphaIfGrown() { int count = _alphaSubmissions.Count; if (count == _alphaSubmitMark) return; _alphaSubmitMark = count; _events.Add(WalkFrameEvent.AlphaSubmitMark(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) }; } }