using System.Numerics; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering.Walk; /// /// Campaign FW stage FW3.2a: the walk→draw population layer. Turns one /// cell's already-classified static content into /// appends (opaque, walk order), per-instance deferred-alpha submissions /// (translucent), and selection-scene publications (picking) — WITHOUT any /// production frame wiring. WorldSceneRenderer does not call this /// class yet; FW3.2b roots it into the real frame. /// /// Why per-entity append instead of the classic material grouping: /// under 's depth-compare Less, opaque draw /// order is pixel-relevant ONLY for coplanar surfaces (a rug on a floor, a /// shell over terrain) — and retail resolves those by first-drawn-wins in /// ITS walk/cell-content order, not by any texture/material grouping. So /// this populator appends one per (entity, /// opaque batch) in the SAME order the caller's records span presents /// them — never material-grouped, never re-sorted. Translucent batches go to /// the SAME the classic/packed paths already /// use (global far→near re-sort still applies; walk-order submission only /// improves retail's submission-order tie-break fidelity for coincident /// distances — WbDrawDispatcher.DeferTransparentGroups's own doc /// comment cites the same CShadowPart::insertion_sort stability this /// preserves). /// /// Reads no retained scene state itself: every method takes the target /// and a caller-supplied span of already /// -queried s (from /// RenderSceneQuery.CopyCellStaticsTo / CopyIndexTo). Owns only /// two small per-call scratch lists — the classify seam's per-entity output /// — reused across records to keep this hot path allocation-free after /// warmup. /// internal sealed class WalkStaticStreamPopulator { private readonly WbDrawDispatcher _dispatcher; private readonly List _batchScratch = new(); private readonly List _selectionScratch = new(); internal WalkStaticStreamPopulator(WbDrawDispatcher dispatcher) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); } /// /// Populates from one indoor cell's static /// content (RenderProjectionClass.IndoorCellStatic — /// RenderSceneQuery.CopyCellStaticsTo). /// is caller-selected per the walk turn this cell's content belongs to /// — for an ordinary /// PView::DrawCells flood, /// for a ConstructView(CBldPortal) look-in, or /// for a building's own /// exterior shell content. /// internal void PopulateCell( OrderedDrawStream stream, WalkDrawStage stage, uint cellId, ReadOnlySpan records, uint tupleLandblockId, Vector3 cameraWorldPosition, Matrix4x4 viewProjection, IWalkLookInViewSource? views = null, int viewRouteIndex = -1, List? alphaSubmissions = null) { ArgumentNullException.ThrowIfNull(stream); for (int i = 0; i < records.Length; i++) { ClassifyAndAppend( stream, stage, cellId, in records[i], tupleLandblockId, cameraWorldPosition, viewProjection, liveDynamic: false, views, viewRouteIndex, alphaSubmissions); } } /// /// The landscape entry point: outdoor static content /// (RenderProjectionClass.OutdoorStatic — /// RenderSceneQuery.CopyIndexTo(RenderSceneIndex.OutdoorStatic, ...)) /// at , the terrain-adjacent /// stage LScape::draw visits a landblock's static objects at /// alongside its ground mesh (see that stage value's own doc comment). /// is the OUTDOOR landblock id — outdoor /// statics have no EnvCell of their own, so this is walk-order /// provenance only, not a clip-slot key. /// internal void PopulateOutdoorStatics( OrderedDrawStream stream, uint cellId, ReadOnlySpan records, uint tupleLandblockId, Vector3 cameraWorldPosition, Matrix4x4 viewProjection, IWalkLookInViewSource? views = null, int viewRouteIndex = -1, ISet? drawnOnce = null, List? alphaSubmissions = null) { ArgumentNullException.ThrowIfNull(stream); for (int i = 0; i < records.Length; i++) { if (drawnOnce is not null && !drawnOnce.Add(records[i].Id)) continue; ClassifyAndAppend( stream, WalkDrawStage.OutdoorStatic, cellId, in records[i], tupleLandblockId, cameraWorldPosition, viewProjection, liveDynamic: false, views, viewRouteIndex, alphaSubmissions); } } internal void PopulateCellDynamics( OrderedDrawStream stream, uint cellId, ReadOnlySpan records, uint tupleLandblockId, Vector3 cameraWorldPosition, Matrix4x4 viewProjection, IWalkLookInViewSource? lookInViews = null, int lookInRouteIndex = -1, ISet? drawnOnce = null, List? alphaSubmissions = null) { ArgumentNullException.ThrowIfNull(stream); for (int i = 0; i < records.Length; i++) { if (drawnOnce is not null && !drawnOnce.Add(records[i].Id)) continue; ClassifyAndAppend( stream, WalkDrawStage.Dynamic, cellId, in records[i], tupleLandblockId, cameraWorldPosition, viewProjection, liveDynamic: true, lookInViews, lookInRouteIndex, alphaSubmissions); } } private void ClassifyAndAppend( OrderedDrawStream stream, WalkDrawStage stage, uint cellId, in RenderProjectionRecord record, uint tupleLandblockId, Vector3 cameraWorldPosition, Matrix4x4 viewProjection, bool liveDynamic = false, IWalkLookInViewSource? lookInViews = null, int lookInRouteIndex = -1, List? alphaSubmissions = null) { _batchScratch.Clear(); _selectionScratch.Clear(); _dispatcher.ClassifyEntityForWalk( in record, tupleLandblockId, _batchScratch, _selectionScratch, liveDynamic, lookInViews, lookInRouteIndex, cellId); for (int i = 0; i < _batchScratch.Count; i++) { WbDrawDispatcher.WalkClassifiedBatch batch = _batchScratch[i]; if (batch.IsOpaque) { stream.Append(new OrderedDrawCommand( batch.Key, batch.Transform, stage, cellId, batch.ClipSlot, batch.Lights, batch.IndoorFlag, batch.Alpha, batch.SelectionLighting, batch.DetailCategory)); } else { if (alphaSubmissions is null) { _dispatcher.SubmitWalkAlphaInstance( in batch, cameraWorldPosition, viewProjection); } else { alphaSubmissions.Add(batch); } } } for (int i = 0; i < _selectionScratch.Count; i++) { WbDrawDispatcher.WalkClassifiedSelectionPart part = _selectionScratch[i]; _dispatcher.PublishWalkSelectionPart(in part); } } }