// 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); /// /// 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(); /// 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 (int[] slots in CellIdToViewSlots.Values) ReturnSlots(slots); if (OutsideViewSlices.Length != 0) ReturnSlices(OutsideViewSlices); CellIdToSlot.Clear(); CellIdToViewSlots.Clear(); CellIdToViewSlices.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; } private static Vector4 AabbOf(ViewPolygon poly) => new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY); }