using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Scene; using AcDream.Core.World; namespace AcDream.App.Rendering; /// /// App-layer port of the retail indoor render orchestration: /// SmartBox::RenderNormalMode -> RenderDeviceD3D::DrawInside -> /// PView::DrawInside -> ConstructView -> DrawCells. /// internal sealed class RetailPViewRenderer { private readonly RenderSceneShadowRuntime _renderSceneShadow; private readonly PortalVisibilityFrame _mainPortalFrameScratch = new(); private readonly ClipFrameAssembly _clipAssemblyScratch = new(); private readonly RetailPViewFrameResult _frameResultScratch = new(); private static readonly ClipViewSlice NoClipSlice = new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty()); private static readonly ClipViewSlice[] NoClipSlices = { NoClipSlice }; private static readonly IReadOnlySet NoParticleOwners = new HashSet(); private readonly HashSet _cellParticleOwnerScratch = new(); // MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across // frames instead of `new HashSet(pvFrame.OrderedVisibleCells)` every // call. Every walk consumer reads it synchronously in this frame. private readonly HashSet _drawableCellsScratch = new(); // FW3 visual-gate fix: the interior root's dynamics phase, invoked by // the driver's clearInteriorDepth closure at the walk's pre-clear // boundary (retail draws outside objects inside LScape::draw, before // the clear+seals). Assigned per frame around the Replay, always // cleared in finally. private Action? _walkPreClearDynamics; // ACDREAM_PROBE_WALK_ROOT (FW3 visual-gate apparatus, throwaway): the // previous frame's root kind + a post-flip frame countdown so each // interior/outdoor transition dumps 8 frames of rooting facts. private bool? _probeWalkRootPrevOutdoor; private int _probeWalkRootFramesLeft; private ulong _probeWalkRootFrame; // Campaign FW3.2b-2: the walk's production world-data registries // (published/retired by LandblockRenderPublisher) plus the per-frame // driver state. Null until the composition passes them; the static // cutover requires all three. private readonly Walk.WalkBuildingRegistry _walkBuildings; private readonly Walk.WalkLandscapeAssembler _walkLandscape; private readonly CellVisibility _walkCellRegistry; // Campaign FW3.2b-2: the production IWalkFrameWorldData over the retained // scene — owned here (not just inside the driver) because DrawInside also // reads it directly to re-source particle owners for the routes the walk // now draws (plan §FW3 item 4). Non-null exactly when _walkBuildings is. private readonly Walk.WalkProductionWorldData _walkWorldData; internal RetailPViewRenderer( RenderSceneShadowRuntime renderSceneShadow, Walk.WalkBuildingRegistry walkBuildings, Walk.WalkLandscapeAssembler walkLandscape, CellVisibility walkCellRegistry) { _renderSceneShadow = renderSceneShadow ?? throw new ArgumentNullException(nameof(renderSceneShadow)); _walkBuildings = walkBuildings ?? throw new ArgumentNullException(nameof(walkBuildings)); _walkLandscape = walkLandscape ?? throw new ArgumentNullException(nameof(walkLandscape)); _walkCellRegistry = walkCellRegistry ?? throw new ArgumentNullException(nameof(walkCellRegistry)); _walkWorldData = new Walk.WalkProductionWorldData(_walkBuildings); } // T2 (BR-4): retail has NO distance constant on the flood-admission chain // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test + // GetClip + GetVisible only). The old 48 m seed cap is replaced by the // caller's per-building frustum pre-gate on aperture bounds (GameWindow's // gather); seeds themselves are unbounded. private const float OutdoorBuildingSeedDistance = float.PositiveInfinity; internal RetailPViewFrameResult DrawInside( RetailPViewFrameInput ctx, RetailPViewPassExecutor passes) { ArgumentNullException.ThrowIfNull(ctx); ArgumentNullException.ThrowIfNull(passes); passes.BeginFrame(); // Campaign FW4: there is one production renderer and one visibility // owner. The fake/legacy executor path was useful during the cutover, // but retaining it here kept the retired PortalVisibilityBuilder and // look-in seed machinery alive. Fail loudly if composition ever tries // to reintroduce that split. RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor ?? throw new InvalidOperationException( "The retail frame walk requires RetailPViewPassExecutor."); // Compatibility carrier only. Production visibility is populated // exclusively by RetailFrameWalk below; this frame supplies the clip // assembler's outdoor full-screen seed until that carrier is removed. PortalVisibilityFrame pvFrame = _mainPortalFrameScratch; pvFrame.ResetForBuild(); if (ctx.RootCell.IsOutdoorNode) { pvFrame.OutsideView.SetFullScreen(); pvFrame.OrderedVisibleCells.Add(ctx.RootCell.CellId); } var clipAssembly = passes.AssembleClipFrame( pvFrame, _clipAssemblyScratch); // FW4 slice 1: PrepareClipFrame (the one clip-region publication) // moved BELOW the walk block — an interior-rooted walk frame // re-derives the outside-view slices from the walk's own views // first, so the appended slots join the same single publication. // R1: draw EVERY visible cell (retail cell_draw_list), not only the cells the // assembler handed a clip-slot. This feeds the Prepare filter + entity partition, // so every visible cell's shell has a prepared batch and seals — killing the grey // (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells). // Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained). _drawableCellsScratch.Clear(); _drawableCellsScratch.UnionWith(pvFrame.OrderedVisibleCells); var drawableCells = _drawableCellsScratch; passes.UseIndoorMembershipOnlyRouting(); // Campaign FW3.4a: THE ONE WALK. Builds walkContext/walkLandscape/ // walkCameraCell exactly as the pre-FW3.4a pre-walk collection pass // did, then drives WalkFrameDriver.Collect — a SINGLE RetailFrameWalk // pass that both learns the flood/visited-cell set (needed below, // BEFORE prepareCells is finalized, so EnvCellRenderer prepares // batches for every shell the driver will draw later in this same // DrawInside call) and records the walk's draw events for Replay // further down, in DrawWalkDrivenStatics. The former SECOND walk pass // (a dedicated set-collecting sink, run again through this same // driver machinery just to submit) is gone — see WalkFrameDriver's // own doc comment for the FW3.4 perf numbers that motivated this. // _walkWorldData.BeginFrame precedes Collect deliberately: Collect's // stream appends classify records immediately (WalkStaticStreamPopulator // runs at append time, not at Replay time), so the world data must // already be rebuilt for this frame before the walk starts. Walk.WalkFrameDriver? walkDriver = null; WalkProductionLeafRenderer walkLeafRenderer; { Matrix4x4 view = ctx.CameraView; var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33)); (int Width, int Height)? attachment = walkExecutor!.WalkAttachmentExtent; // Fallback matches the FW3.2b-2 shadow probe's own comment: every // screen projection shares the same constants, so this is only // reached before the world pass has published its scope. float viewportWidth = attachment?.Width ?? 1024f; float viewportHeight = attachment?.Height ?? 720f; var walkContext = new Walk.WalkProductionFrameContext( _walkCellRegistry!, _walkBuildings!, ctx.ViewerEyePos, forward, ctx.ViewProjection, viewportWidth, viewportHeight); _walkLandscape!.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos); Walk.WalkLandscape walkLandscape = _walkLandscape.Landscape; Walk.WalkCell? walkCameraCell = null; if ((ctx.ViewerCellId & 0xFFFFu) >= 0x100) { walkCameraCell = _walkCellRegistry!.TryGetCell(ctx.ViewerCellId, out LoadedCell? loaded) ? loaded?.Walk : null; if (walkCameraCell is null) { throw new InvalidOperationException( $"walk root=0x{ctx.ViewerCellId:X8}: the interior camera cell has " + "no committed walk data — the Campaign FW3.2b-2 static cutover " + "requires the walk registry to already hold the viewer's own cell " + "(fail-loud rule; a silently skipped root would leave the frame " + "with no static draws at all)."); } } if (ctx.RootCell.IsOutdoorNode && clipAssembly.OutsideViewSlices.Length != 1) { throw new InvalidOperationException( "walk static cutover: an outdoor root's clip assembly produced " + $"{clipAssembly.OutsideViewSlices.Length} outside-view slices, not the " + "expected 1 — the outdoor root draws through the full-screen default " + "view (retail set_default_view; the walk fans exactly its own 1), and " + "the outdoor slice data still comes from the assembler (plan §FW3 item " + "2c's pinned assumption; assert rather than silently coercing to 1)."); } _walkWorldData!.BeginFrame( _renderSceneShadow!.Query, ctx.PlayerLandblockId ?? 0u, ctx.RenderCenterLbX, ctx.RenderCenterLbY); Action clearInteriorDepth = () => { // FW3 visual-gate fix (owner report: doors/candles invisible // looking out; the crossing vanish): retail draws the // OUTSIDE world's objects INSIDE LScape::draw — strictly // BEFORE the depth clear + seals (the #118 house-exit // clip+vanish lesson: anything drawn after the seals z-fails // against their true-depth stamp the moment it stands beyond // the door plane). The surviving dynamic routes + outdoor // particles + weather therefore run HERE, at the walk's // pre-clear boundary, for an interior root. _walkPreClearDynamics?.Invoke(); // Retail PView::DrawCells 0x005A4872 drains the landscape // alpha list immediately before the gated full depth clear — // mirrors DrawLandscapeThroughOutsideView's own pre-clear // drain. passes.FlushLandscapeAlpha(); passes.ClearInteriorDepth(); }; // FW4 slice 2: the seals stamp the WALK'S OWN flood cells (see // DrawWalkExitPortalMasks). walkDriver is assigned below, before // any Replay can fire this closure. Action drawExitSeals = () => DrawWalkExitPortalMasks(ctx, passes, clipAssembly, walkDriver!); walkLeafRenderer = new WalkProductionLeafRenderer( walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals); walkDriver = new Walk.WalkFrameDriver( walkExecutor!.Dispatcher, walkLeafRenderer, _walkWorldData); if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) { AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame = _probeWalkRootFrame % 90 == 0; } walkDriver.Collect( _frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext, ctx.ViewProjection, ctx.CameraWorldPosition); AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame = false; // FW4 slice 1: an interior root's terrain/sky/punch clip slices // come from THE WALK'S OWN outside_view — retail's one // visibility structure. See ClipFrameAssembler. // ReassembleOutsideViewFromWalk's doc comment for the boundary- // frame desync (the stairwell/grass flash) this retires. The // outdoor root keeps the assembler's single full-screen slice // (asserted ==1 above; identical content by construction). if (walkCameraCell is not null) { ClipFrameAssembler.ReassembleOutsideViewFromWalk( clipAssembly, _frameWalk.InteriorOutsideView, viewportWidth, viewportHeight); } // The walk's visited set replaces PortalVisibilityFrame's old // OrderedVisibleCells side-channel for every production consumer. _drawableCellsScratch.Clear(); _drawableCellsScratch.UnionWith(walkDriver.VisitedCells); // Phase I cathedral instrumentation (synthesis §Phase I.3): the // continuous rooting line SEPARATES the true root flood // (InteriorFloodCells) from the visited union (root + look-ins — // the conflation the review corpus indicted), and adds the // walk's OWN exit-view count. [walk-cam] dumps a replayable // camera every ~300 frames and at every root flip. if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled) { _probeWalkRootFrame++; bool outdoorNow = ctx.RootCell.IsOutdoorNode; bool flipped = _probeWalkRootPrevOutdoor is bool prev && prev != outdoorNow; if (flipped) { _probeWalkRootFramesLeft = 8; Console.WriteLine( $"[walk-root] ---- FLIP {(outdoorNow ? "IN->OUT" : "OUT->IN")} at frame {_probeWalkRootFrame} ----"); } _probeWalkRootPrevOutdoor = outdoorNow; bool sample = _probeWalkRootFramesLeft > 0 || _probeWalkRootFrame % 30 == 0; if (_probeWalkRootFramesLeft > 0) _probeWalkRootFramesLeft--; if (sample) { List rootFlood = walkDriver.InteriorFloodCells; string flood = string.Join(",", rootFlood .GetRange(0, Math.Min(rootFlood.Count, 8)) .ConvertAll(c => c.ToString("x8"))); int walkOv = outdoorNow ? -1 : _frameWalk.InteriorOutsideView.ViewCount; Vector3 fwd = walkContext.CyPlane.Normal; Console.WriteLine( $"[walk-root] f={_probeWalkRootFrame} out={(outdoorNow ? 1 : 0)} " + $"viewer=0x{ctx.ViewerCellId:X8} root=0x{ctx.RootCell.CellId:X8} " + $"res={ctx.CameraCellResolution} slices={clipAssembly.OutsideViewSlices.Length} " + $"walkOv={walkOv} rootFlood={rootFlood.Count}[{flood}] " + $"visited={walkDriver.VisitedCells.Count} bld={walkDriver.VisitedBuildings.Count} " + $"eye=({ctx.ViewerEyePos.X:F2},{ctx.ViewerEyePos.Y:F2},{ctx.ViewerEyePos.Z:F2}) " + $"fwd=({fwd.X:F3},{fwd.Y:F3},{fwd.Z:F3})"); } if (flipped || _probeWalkRootFrame % 300 == 0) { Matrix4x4 vp = ctx.ViewProjection; Console.WriteLine( $"[walk-cam] viewer=0x{ctx.ViewerCellId:X8} " + $"eye=({ctx.ViewerEyePos.X:R},{ctx.ViewerEyePos.Y:R},{ctx.ViewerEyePos.Z:R}) " + $"vw={viewportWidth:R} vh={viewportHeight:R} vp=[" + $"{vp.M11:R},{vp.M12:R},{vp.M13:R},{vp.M14:R}," + $"{vp.M21:R},{vp.M22:R},{vp.M23:R},{vp.M24:R}," + $"{vp.M31:R},{vp.M32:R},{vp.M33:R},{vp.M34:R}," + $"{vp.M41:R},{vp.M42:R},{vp.M43:R},{vp.M44:R}]"); } } } // FW4 slice 1: the ONE clip-region publication, after any walk // reassembly so the walk-derived outside-view slots are included // (moved from directly after AssembleClipFrame; the count is // reservation metadata the RHI arm ignores). int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2); passes.PrepareClipFrame(terrainUploadCount); // Production prepares exactly the one walk's visited-cell set. HashSet prepareCells = drawableCells; // (#176 correction, 2026-07-06: the flood-scoped light-pool rebuild that ran // here was the seam-floor flicker mechanism — retail's visible_cell_table is // the RESIDENT-cell registry, not the frame flood — and is deleted. The pool // is built once per frame in GameWindow, player-anchored.) passes.PrepareCellBatches(ctx, prepareCells); { RenderProjectionCounts retainedCounts = _renderSceneShadow.Counts; RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts); RenderProjectionCounts sourceCounts = retainedCounts; RetailPViewFrameResult result = _frameResultScratch.Reset( pvFrame, clipAssembly, drawableCells, prepareCells, counts, sourceCounts, diagnosticPartition: null); passes.EmitDiagnostics(ctx, result); // The one collected walk owns terrain, cell shells, buildings, // statics, dynamics, particles, punches, and alpha barriers at // their retail turns. Interior-root landscape services run at the // pre-clear callback; outdoor roots have no clear and run after // replay. _walkPreClearDynamics = () => { passes.UseIndoorMembershipOnlyRouting(); DrawLandscapeDynamicsPhase( ctx, passes, clipAssembly); }; try { DrawWalkDrivenStatics(ctx, walkExecutor, walkDriver!); passes.UseIndoorMembershipOnlyRouting(); if (ctx.RootCell.IsOutdoorNode) { DrawLandscapeDynamicsPhase( ctx, passes, clipAssembly); } } finally { _walkPreClearDynamics = null; } // OUTDOOR root: the LScape-boundary alpha drain deferred from the // landscape stage runs HERE, after punches, interior shells, cell // objects, and the dynamics pass — the frame's complete opaque // world. Retail's walk draws all of those before its boundary // flush (LScape::draw includes every cell's objects, // DrawSortCell 0x005A17C0), so this is the same one-list far→near // composite over finished depth; draining at the stage end instead // let every later opaque mesh overwrite the flames (#132). if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha(); // Interior-cell UNATTACHED emitters (spell ground effects and // swirls anchored in EnvCells) draw in this final world scope — // the cells' walls and the seals already own the depth buffer, so // one unclipped submission matches retail's cell-walk insertion. // Outdoor-cell unattached emitters drew in the landscape stage. passes.DrawUnattachedSceneParticles(ctx, outdoorCells: false); return result; } } private readonly Walk.RetailFrameWalk _frameWalk = new(); /// Campaign FW3.2b-2 — THE PRODUCTION ROOTING; Campaign FW3.4a — /// REPLAY ONLY. already ran its Collect pass /// earlier in (before PrepareCellBatches); /// this method's job is now just /// (terrain/sky, every building's shell + punch + look-in cell statics, /// and the interior root's own flood shells + statics, with retail's /// depth-clear/exit-seal turn — all in walk order, replacing /// 's static half, /// 's old top-level call, /// , and 's /// static half for this frame) plus re-sourcing the particle owners the /// routes it replaced used to ride, from the driver's own visited /// sets (plan §FW3 "FW3.2b-2 — the production rooting", items 2 and /// 4). private void DrawWalkDrivenStatics( RetailPViewFrameInput ctx, RetailPViewPassExecutor passes, Walk.WalkFrameDriver driver) { var (frame, encoder) = passes.RequireWalkSubmission(); passes.SetWalkTerrainInViewLandcells(driver.VisitedLandscapeCellIds); try { driver.Replay(frame, encoder); } finally { passes.SetWalkTerrainInViewLandcells(null); } // Landscape-stage static-owner particles (candles, the cathedral // falls) submit AT THEIR OWN WALK TURNS inside Replay // (WalkFrameEventKind.StaticParticles) for BOTH root kinds — the // #132 positional invariant (e102fb36's user-verified rule ported // walk-natively): each owner cell's emitters enter the one alpha // queue at that cell's far-to-near position, so every nearer // building's pre-punch AlphaBarrier drains the farther content // against still-true depth before its punch stamps far-Z. // Cell-stage particle owners for the interior root's OWN flood cells // (non-look-in — the walk draws their statics too, via // OnInteriorFloodDrawTurn/EmitCellTurn, so the retired CellStatic // route's cell-particle submission needs the same re-sourcing). // These stay POST-replay: interior emitters draw in the final world // scope where the cell walls already own depth (retail's cell-walk // insertion). Look-in cells get their OWN per-cell union in // DrawBuildingLookInDynamics so a static owner is never submitted // twice. _cellParticleOwnerScratch.Clear(); foreach (uint cellId in driver.VisitedCells) { if (driver.LookInCells.Contains(cellId)) continue; UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch); } if (_cellParticleOwnerScratch.Count > 0) { passes.DrawCellParticles( ctx, new RetailPViewCellSliceContext(0u, NoClipSlice, _cellParticleOwnerScratch)); } } private static void UnionRecordOwners( Walk.WalkFrameStaticRecords records, HashSet destination) { foreach (RenderProjectionRecord record in records.Records) { if (record.Source.LocalEntityId != 0) destination.Add(record.Source.LocalEntityId); } } /// Campaign FW3.2b-2: the DYNAMICS-only remainder of the old /// + /// split. The walk now draws every mesh and every attached emitter at its /// own turn. This tail therefore carries only the two landscape services /// that are not scene projections: ownerless outdoor-cell emitters and /// weather. It deliberately submits no packed entity route. private void DrawLandscapeDynamicsPhase( RetailPViewFrameInput ctx, RetailPViewPassExecutor passes, ClipFrameAssembly clipAssembly) { if (clipAssembly.OutsideViewSlices.Length == 0) return; // Ownerless OUTDOOR-cell emitters — now unconditional: the old // hasBuildingLookIns gate only existed to sequence this submission // around the OLD static barrier drains, which no longer run here // (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn). passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true); // GameSky's weather pass still runs through each active landscape // view so doorway scissor/clip state matches the old executor. The // dynamics collection is intentionally empty: the walk already drew // those records at OnLandscapeCellTurn. foreach (var slice in clipAssembly.OutsideViewSlices) { passes.SetTerrainClip(slice.Planes); passes.ClearClipRouting(); passes.DrawLandscapeSliceLate( ctx, new RetailPViewLandscapeLateSliceContext( slice, Array.Empty())); } passes.UseIndoorMembershipOnlyRouting(); } /// Campaign FW4 slice 2 — the walk-flood seal draw. Retail's /// PView::DrawCells stamps every exit portal of THE FLOOD'S OWN /// cells (pc:432785-432786, reverse cell_draw_list far→near) — one /// visibility structure decides the flood, the seals, and the terrain /// views alike. The old apparatus's flood misses exit portals at the /// #456 cathedral seam band (its never-drawn panel family), leaving /// aperture depth unsealed after the interior clear; the end-of-frame /// alpha drain (cell-owned emitters — retail's own timing) then /// z-passes across the whole opening (the falls shine-through, /// probe-pinned via ACDREAM_PROBE_WALK_ROOT's phase tags). Per-cell /// slice clips still come from the old assembly where present; a cell /// the old apparatus missed seals unclipped (the depth fan is the exact /// dat aperture polygon and z-tests, so over-coverage is benign). private void DrawWalkExitPortalMasks( RetailPViewFrameInput ctx, RetailPViewPassExecutor passes, ClipFrameAssembly clipAssembly, Walk.WalkFrameDriver driver) { List floodCells = driver.InteriorFloodCells; for (int i = floodCells.Count - 1; i >= 0; i--) { uint cellId = floodCells[i]; foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) passes.DrawExitPortalMask( ctx, new RetailPViewCellSliceContext( cellId, slice, NoParticleOwners)); } } private static RenderFrameDiagnosticCounts WalkDiagnosticCounts( RenderProjectionCounts source) { int dynamics = checked( source.LiveDynamicRoot + source.ActiveAnimatedStatic + source.EquippedChild); return new RenderFrameDiagnosticCounts( source.OutdoorStatic, source.IndoorCellStatic, dynamics, TransformCount: source.Total, OpaqueClassificationCount: 0, AlphaClassificationCount: 0, LightSetCount: 0, SelectionPartCount: 0, RouteCandidateCount: source.Total, EntityCandidateCount: source.Total, MeshPartCount: 0); } private static ClipViewSlice[] GetCellSlicesOrNoClip( ClipFrameAssembly clipAssembly, uint cellId) { if (clipAssembly.CellIdToViewSlices.TryGetValue(cellId, out var slices) && slices.Length > 0) { return slices; } return NoClipSlices; } } public interface IRetailPViewCellSource { LoadedCell? Find(uint cellId); } /// /// Typed execution seam for the GL passes ordered by /// . Implementations execute the requested /// pass only; visibility construction and draw ordering remain renderer-owned. /// All frame inputs and results are borrowed for the duration of the call. /// public sealed class RetailPViewFrameInput { public LoadedCell RootCell { get; private set; } = null!; /// R-A2: nearby building cells (BuildingId-tagged) flooded per-building when the root is the /// outdoor node. Null for interior roots. Grouped by BuildingId inside . public IReadOnlyList? NearbyBuildingCells { get; private set; } public Vector3 ViewerEyePos { get; private set; } public Matrix4x4 ViewProjection { get; private set; } public IRetailPViewCellSource Cells { get; private set; } = null!; public ICamera Camera { get; private set; } = null!; public Vector3 CameraWorldPosition { get; private set; } public FrustumPlanes? Frustum { get; private set; } public uint? PlayerLandblockId { get; private set; } public HashSet? AnimatedEntityIds { get; private set; } public int RenderCenterLbX { get; private set; } public int RenderCenterLbY { get; private set; } public int RenderRadius { get; private set; } public IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, IReadOnlyDictionary? AnimatedById)> LandblockEntries { get; private set; } = Array.Empty<(uint, Vector3, Vector3, IReadOnlyList, IReadOnlyDictionary?)>(); // Pass-presentation and diagnostic values consumed synchronously by the // typed executor. This input is data-only and is never retained. public bool RenderSky { get; private set; } public bool RenderWeather { get; private set; } public float DayFraction { get; private set; } public DayGroupData? ActiveDayGroup { get; private set; } public SkyKeyframe SkyKeyframe { get; private set; } public bool EnvironOverrideActive { get; private set; } public uint ViewerCellId { get; private set; } public uint PlayerCellId { get; private set; } public Vector3 PlayerViewPosition { get; private set; } public Matrix4x4 CameraView { get; private set; } public CameraCellResolution CameraCellResolution { get; private set; } internal RetailPViewFrameInput Reset( LoadedCell rootCell, IReadOnlyList? nearbyBuildingCells, Vector3 viewerEyePos, Matrix4x4 viewProjection, IRetailPViewCellSource cells, ICamera camera, Vector3 cameraWorldPosition, FrustumPlanes? frustum, uint? playerLandblockId, HashSet? animatedEntityIds, int renderCenterLbX, int renderCenterLbY, int renderRadius, IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities, IReadOnlyDictionary? AnimatedById)> landblockEntries, bool renderSky, bool renderWeather, float dayFraction, DayGroupData? activeDayGroup, SkyKeyframe skyKeyframe, bool environOverrideActive, uint viewerCellId, uint playerCellId, Vector3 playerViewPosition, Matrix4x4 cameraView, CameraCellResolution cameraCellResolution) { RootCell = rootCell; NearbyBuildingCells = nearbyBuildingCells; ViewerEyePos = viewerEyePos; ViewProjection = viewProjection; Cells = cells; Camera = camera; CameraWorldPosition = cameraWorldPosition; Frustum = frustum; PlayerLandblockId = playerLandblockId; AnimatedEntityIds = animatedEntityIds; RenderCenterLbX = renderCenterLbX; RenderCenterLbY = renderCenterLbY; RenderRadius = renderRadius; LandblockEntries = landblockEntries; RenderSky = renderSky; RenderWeather = renderWeather; DayFraction = dayFraction; ActiveDayGroup = activeDayGroup; SkyKeyframe = skyKeyframe; EnvironOverrideActive = environOverrideActive; ViewerCellId = viewerCellId; PlayerCellId = playerCellId; PlayerViewPosition = playerViewPosition; CameraView = cameraView; CameraCellResolution = cameraCellResolution; return this; } } /// /// Borrowed renderer scratch valid only until the next /// call. Collections and nested /// frame objects are deliberately reused to keep the render loop allocation /// free; consumers must copy any state they need to retain asynchronously. /// public sealed class RetailPViewFrameResult { public PortalVisibilityFrame PortalFrame { get; private set; } = null!; public ClipFrameAssembly ClipAssembly { get; private set; } = null!; public HashSet DrawableCells { get; private set; } = null!; /// /// The production retail walk's exact visited-cell set. This is the one /// visibility answer consumed by EnvCell preparation, particles, lights, /// and directional-shadow filtering. /// public HashSet VisibleCells { get; private set; } = null!; internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; } internal RenderProjectionCounts SourceCounts { get; private set; } internal InteriorEntityPartition.Result? DiagnosticPartition { get; private set; } internal RetailPViewFrameResult Reset( PortalVisibilityFrame portalFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells, HashSet visibleCells, RenderFrameDiagnosticCounts diagnosticCounts, RenderProjectionCounts sourceCounts, InteriorEntityPartition.Result? diagnosticPartition) { PortalFrame = portalFrame; ClipAssembly = clipAssembly; DrawableCells = drawableCells; VisibleCells = visibleCells; DiagnosticCounts = diagnosticCounts; SourceCounts = sourceCounts; DiagnosticPartition = diagnosticPartition; return this; } } /// /// Scene-particle owners for ONE unclipped landscape-stage submission (the /// union of every outside slice's cone survivors). Mesh alpha for the same /// owners is already queued by the entity routes; retail inserts each /// emitter's polys into the single alpha list once, during its owner cell's /// walk turn, with no portal-view clip. /// public readonly record struct RetailPViewLandscapeStaticParticleContext( IReadOnlySet ParticleOwnerIds); /// #131/#132: the late landscape phase's per-slice payload — /// outside-stage dynamics to mesh-draw, plus the particle owners not already /// submitted at a pre-building barrier. public readonly record struct RetailPViewLandscapeLateSliceContext( ClipViewSlice Slice, IReadOnlyList Dynamics); public readonly record struct RetailPViewCellSliceContext( uint CellId, ClipViewSlice Slice, IReadOnlySet ParticleOwnerIds);