// ClipFrameAssembler.cs // // Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like // view graph: one portal_view list per visible cell plus an outside_view list. // This assembler packs each visible polygon as an individual GPU clip slot so // the renderer can draw the exact PView order: // // outside_view landscape slices // reverse cell_draw_list exit masks // reverse cell_draw_list EnvCell shells // reverse cell_draw_list object lists // // Slot 0 is always no-clip. A slice whose polygon cannot be represented by the // <=8 plane budget uses slot 0 and its NDC AABB; the renderer uses scissor for // passes that need that fallback. Empty regions are omitted entirely. using System.Collections.Generic; using System.Numerics; namespace AcDream.App.Rendering; /// /// How the landscape-through-outside_view pass should be interpreted. /// public enum TerrainClipMode { /// All outside_view slices have convex plane clips. Planes, /// At least one outside_view slice requires scissor fallback. Scissor, /// No outside_view slice is visible; skip landscape indoors. Skip, } /// /// One retail portal_view slice mapped to a GPU clip slot. The AABB is retained /// for passes that cannot write gl_ClipDistance and must use scissor. /// public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes); /// /// Identifies one cell inside one nested building look-in. The same EnvCell can /// be reached by more than one building PView, so a cell id alone is not a /// sufficient routing key. /// public readonly record struct LookInClipCell(int FrameIndex, uint CellId); /// /// Result of : populated clip buffers /// plus routing data consumed by the render orchestration. /// public sealed class ClipFrameAssembly { public ClipFrame Frame { get; private set; } = null!; /// First drawable slice slot per visible cell. Compatibility map /// for renderer APIs that can accept only one slot at a time. public Dictionary CellIdToSlot { get; } = new(); /// Slot-only cell slices, retained for older renderer APIs. public Dictionary CellIdToViewSlots { get; } = new(); /// Full retail portal_view slices per visible cell. public Dictionary CellIdToViewSlices { get; } = new(); /// First drawable slice slot per nested look-in cell. public Dictionary LookInCellToSlot { get; } = new(); /// All retail portal_view slices per nested look-in cell. public Dictionary LookInCellToViewSlices { get; } = new(); /// Full retail outside_view slices. public ClipViewSlice[] OutsideViewSlices { get; private set; } = System.Array.Empty(); public int OutdoorSlot { get; internal set; } public bool OutdoorVisible { get; internal set; } public TerrainClipMode TerrainMode { get; internal set; } public Vector4 TerrainScissorNdcAabb { get; internal set; } public bool HasOutsideView { get; internal set; } public Vector4 OutsideViewNdcAabb { get; internal set; } // Probe data. public int OutsidePlaneCount { get; internal set; } public Dictionary PerCellPlaneCounts { get; } = new(); public int ScissorFallbacks { get; internal set; } // The assembly is frame-scoped. An owner that passes it back to Assemble may reuse the // dictionaries, per-cell arrays, and construction list after the prior frame is consumed. // Exact-length pools keep the public array API honest: Length is always the live slice count. private readonly Dictionary> _sliceArraysByLength = new(); private readonly Dictionary> _slotArraysByLength = new(); internal List SliceScratch { get; } = new(); internal int SliceArrayAllocationCount { get; private set; } internal int SlotArrayAllocationCount { get; private set; } internal const int MaxRetainedSliceItems = 4096; internal const int MaxRetainedSlotItems = 8192; internal const int MaxRetainedArraysPerPool = 128; internal int RetainedSliceItems { get; private set; } internal int RetainedSlotItems { get; private set; } internal int RetainedSliceArrays { get; private set; } internal int RetainedSlotArrays { get; private set; } internal void Reset(ClipFrame frame) { Frame = frame; foreach (ClipViewSlice[] slices in CellIdToViewSlices.Values) ReturnSlices(slices); foreach (ClipViewSlice[] slices in LookInCellToViewSlices.Values) ReturnSlices(slices); foreach (int[] slots in CellIdToViewSlots.Values) ReturnSlots(slots); if (OutsideViewSlices.Length != 0) ReturnSlices(OutsideViewSlices); CellIdToSlot.Clear(); CellIdToViewSlots.Clear(); CellIdToViewSlices.Clear(); LookInCellToSlot.Clear(); LookInCellToViewSlices.Clear(); PerCellPlaneCounts.Clear(); OutsideViewSlices = System.Array.Empty(); SliceScratch.Clear(); } internal ClipViewSlice[] CopySlices(List source) { if (source.Count == 0) return System.Array.Empty(); ClipViewSlice[] result = RentSlices(source.Count); source.CopyTo(result, 0); return result; } internal int[] CopySlots(ClipViewSlice[] slices) { if (slices.Length == 0) return System.Array.Empty(); int[] result = RentSlots(slices.Length); for (int i = 0; i < slices.Length; i++) result[i] = slices[i].Slot; return result; } internal void SetOutsideViewSlices(ClipViewSlice[] slices) => OutsideViewSlices = slices; private ClipViewSlice[] RentSlices(int length) { if (_sliceArraysByLength.TryGetValue(length, out Stack? pool) && pool.Count != 0) { ClipViewSlice[] result = pool.Pop(); RetainedSliceItems -= result.Length; RetainedSliceArrays--; if (pool.Count == 0) _sliceArraysByLength.Remove(length); return result; } SliceArrayAllocationCount++; return new ClipViewSlice[length]; } private int[] RentSlots(int length) { if (_slotArraysByLength.TryGetValue(length, out Stack? pool) && pool.Count != 0) { int[] result = pool.Pop(); RetainedSlotItems -= result.Length; RetainedSlotArrays--; if (pool.Count == 0) _slotArraysByLength.Remove(length); return result; } SlotArrayAllocationCount++; return new int[length]; } private void ReturnSlices(ClipViewSlice[] array) { // ClipViewSlice contains a Vector4[] reference. Clear before either retaining or dropping // the array so a bounded cache cannot accidentally extend plane-payload lifetimes. System.Array.Clear(array); if (array.Length > MaxRetainedSliceItems) return; while (RetainedSliceItems + array.Length > MaxRetainedSliceItems || RetainedSliceArrays >= MaxRetainedArraysPerPool) { if (!EvictOneSliceArray()) break; } if (!_sliceArraysByLength.TryGetValue(array.Length, out Stack? pool)) { pool = new Stack(); _sliceArraysByLength.Add(array.Length, pool); } pool.Push(array); RetainedSliceItems += array.Length; RetainedSliceArrays++; } private void ReturnSlots(int[] array) { if (array.Length > MaxRetainedSlotItems) return; while (RetainedSlotItems + array.Length > MaxRetainedSlotItems || RetainedSlotArrays >= MaxRetainedArraysPerPool) { if (!EvictOneSlotArray()) break; } if (!_slotArraysByLength.TryGetValue(array.Length, out Stack? pool)) { pool = new Stack(); _slotArraysByLength.Add(array.Length, pool); } pool.Push(array); RetainedSlotItems += array.Length; RetainedSlotArrays++; } private bool EvictOneSliceArray() { int selectedLength = -1; foreach ((int length, Stack pool) in _sliceArraysByLength) { if (pool.Count != 0 && length > selectedLength) selectedLength = length; } if (selectedLength < 0) return false; Stack selected = _sliceArraysByLength[selectedLength]; ClipViewSlice[] evicted = selected.Pop(); RetainedSliceItems -= evicted.Length; RetainedSliceArrays--; if (selected.Count == 0) _sliceArraysByLength.Remove(selectedLength); return true; } private bool EvictOneSlotArray() { int selectedLength = -1; foreach ((int length, Stack pool) in _slotArraysByLength) { if (pool.Count != 0 && length > selectedLength) selectedLength = length; } if (selectedLength < 0) return false; Stack selected = _slotArraysByLength[selectedLength]; int[] evicted = selected.Pop(); RetainedSlotItems -= evicted.Length; RetainedSlotArrays--; if (selected.Count == 0) _slotArraysByLength.Remove(selectedLength); return true; } } public static class ClipFrameAssembler { public static ClipFrameAssembly Assemble( ClipFrame frame, PortalVisibilityFrame pvFrame, ClipFrameAssembly? reuseAssembly = null) { System.ArgumentNullException.ThrowIfNull(frame); System.ArgumentNullException.ThrowIfNull(pvFrame); frame.Reset(); ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly(); assembly.Reset(frame); Dictionary cellIdToSlot = assembly.CellIdToSlot; Dictionary cellIdToViewSlots = assembly.CellIdToViewSlots; Dictionary cellIdToViewSlices = assembly.CellIdToViewSlices; Dictionary perCellPlaneCounts = assembly.PerCellPlaneCounts; int scissorFallbacks = 0; foreach (uint cellId in pvFrame.OrderedVisibleCells) { if (!pvFrame.CellViews.TryGetValue(cellId, out var view)) continue; List slices = assembly.SliceScratch; slices.Clear(); int maxPlaneCount = 0; foreach (var poly in view.Polygons) { var cps = ClipPlaneSet.From(poly); if (cps.IsNothingVisible) continue; int slot; Vector4[] planes; if (cps.Count > 0) { planes = cps.PlaneArray; slot = frame.AppendSlot(planes); if (cps.Count > maxPlaneCount) maxPlaneCount = cps.Count; } else { planes = System.Array.Empty(); slot = 0; scissorFallbacks++; } slices.Add(new ClipViewSlice(slot, AabbOf(poly), planes)); } if (slices.Count == 0) continue; ClipViewSlice[] sliceArray = assembly.CopySlices(slices); cellIdToViewSlices[cellId] = sliceArray; cellIdToViewSlots[cellId] = assembly.CopySlots(sliceArray); cellIdToSlot[cellId] = sliceArray[0].Slot; perCellPlaneCounts[cellId] = maxPlaneCount; } List outsideSlicesList = assembly.SliceScratch; outsideSlicesList.Clear(); int outsideMaxPlaneCount = 0; bool outsideHasScissorFallback = false; foreach (var poly in pvFrame.OutsideView.Polygons) { var cps = ClipPlaneSet.From(poly); if (cps.IsNothingVisible) continue; int slot; Vector4[] planes; if (cps.Count > 0) { planes = cps.PlaneArray; slot = frame.AppendSlot(planes); if (cps.Count > outsideMaxPlaneCount) outsideMaxPlaneCount = cps.Count; } else { planes = System.Array.Empty(); slot = 0; outsideHasScissorFallback = true; scissorFallbacks++; } outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes)); } ClipViewSlice[] outsideViewSlices = assembly.CopySlices(outsideSlicesList); bool outdoorVisible = outsideViewSlices.Length > 0; int outdoorSlot = outdoorVisible ? outsideViewSlices[0].Slot : 0; TerrainClipMode terrainMode = !outdoorVisible ? TerrainClipMode.Skip : (outsideHasScissorFallback ? TerrainClipMode.Scissor : TerrainClipMode.Planes); Vector4 outsideViewNdcAabb = outdoorVisible ? new Vector4(pvFrame.OutsideView.MinX, pvFrame.OutsideView.MinY, pvFrame.OutsideView.MaxX, pvFrame.OutsideView.MaxY) : Vector4.Zero; Vector4 terrainScissor = terrainMode == TerrainClipMode.Scissor ? outsideViewNdcAabb : Vector4.Zero; assembly.SetOutsideViewSlices(outsideViewSlices); assembly.OutdoorSlot = outdoorSlot; assembly.OutdoorVisible = outdoorVisible; assembly.TerrainMode = terrainMode; assembly.TerrainScissorNdcAabb = terrainScissor; assembly.HasOutsideView = outdoorVisible; assembly.OutsideViewNdcAabb = outsideViewNdcAabb; assembly.OutsidePlaneCount = terrainMode == TerrainClipMode.Planes ? outsideMaxPlaneCount : 0; assembly.ScissorFallbacks = scissorFallbacks; return assembly; } /// /// Appends the cell views used by nested DrawBuilding -> DrawPortal /// PViews to the already assembled frame. Retail installs each nested /// cell's own portal_view before drawing its shell and object list; /// these slots must therefore be published with the main frame before the /// first draw is recorded. /// public static void AppendLookInFrames( ClipFrame frame, IReadOnlyList lookInFrames, ClipFrameAssembly assembly) { System.ArgumentNullException.ThrowIfNull(frame); System.ArgumentNullException.ThrowIfNull(lookInFrames); System.ArgumentNullException.ThrowIfNull(assembly); if (!ReferenceEquals(frame, assembly.Frame)) throw new System.ArgumentException( "The look-in slots must be appended to the assembly's clip frame.", nameof(frame)); for (int frameIndex = 0; frameIndex < lookInFrames.Count; frameIndex++) { PortalVisibilityFrame lookIn = lookInFrames[frameIndex]; foreach (uint cellId in lookIn.OrderedVisibleCells) { if (!lookIn.CellViews.TryGetValue(cellId, out CellView? view)) continue; List slices = assembly.SliceScratch; slices.Clear(); foreach (ViewPolygon poly in view.Polygons) { ClipPlaneSet cps = ClipPlaneSet.From(poly); if (cps.IsNothingVisible) continue; int slot; Vector4[] planes; if (cps.Count > 0) { planes = cps.PlaneArray; slot = frame.AppendSlot(planes); } else { planes = System.Array.Empty(); slot = 0; assembly.ScissorFallbacks++; } slices.Add(new ClipViewSlice(slot, AabbOf(poly), planes)); } if (slices.Count == 0) continue; ClipViewSlice[] packed = assembly.CopySlices(slices); var key = new LookInClipCell(frameIndex, cellId); assembly.LookInCellToViewSlices.Add(key, packed); assembly.LookInCellToSlot.Add(key, packed[0].Slot); } } } private static Vector4 AabbOf(ViewPolygon poly) => new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY); }