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);