// 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 required ClipFrame Frame { get; init; }
/// First drawable slice slot per visible cell. Compatibility map
/// for renderer APIs that can accept only one slot at a time.
public required Dictionary CellIdToSlot { get; init; }
/// Slot-only cell slices, retained for older renderer APIs.
public required Dictionary CellIdToViewSlots { get; init; }
/// Full retail portal_view slices per visible cell.
public required Dictionary CellIdToViewSlices { get; init; }
/// Full retail outside_view slices.
public required ClipViewSlice[] OutsideViewSlices { get; init; }
public required int OutdoorSlot { get; init; }
public required bool OutdoorVisible { get; init; }
public required TerrainClipMode TerrainMode { get; init; }
public required Vector4 TerrainScissorNdcAabb { get; init; }
public required bool HasOutsideView { get; init; }
public required Vector4 OutsideViewNdcAabb { get; init; }
// Probe data.
public required int OutsidePlaneCount { get; init; }
public required Dictionary PerCellPlaneCounts { get; init; }
public required int ScissorFallbacks { get; init; }
}
public static class ClipFrameAssembler
{
public static ClipFrameAssembly Assemble(ClipFrame frame, PortalVisibilityFrame pvFrame)
{
System.ArgumentNullException.ThrowIfNull(frame);
System.ArgumentNullException.ThrowIfNull(pvFrame);
frame.Reset();
var cellIdToSlot = new Dictionary();
var cellIdToViewSlots = new Dictionary();
var cellIdToViewSlices = new Dictionary();
var perCellPlaneCounts = new Dictionary();
int scissorFallbacks = 0;
foreach (uint cellId in pvFrame.OrderedVisibleCells)
{
if (!pvFrame.CellViews.TryGetValue(cellId, out var view))
continue;
var slices = new List(view.Polygons.Count);
int maxPlaneCount = 0;
foreach (var poly in view.Polygons)
{
var cps = ClipPlaneSet.From(ViewOf(poly));
if (cps.IsNothingVisible)
continue;
int slot;
Vector4[] planes;
if (cps.Count > 0)
{
planes = ToPlaneSpan(cps);
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;
var sliceArray = slices.ToArray();
cellIdToViewSlices[cellId] = sliceArray;
cellIdToViewSlots[cellId] = ToSlots(sliceArray);
cellIdToSlot[cellId] = sliceArray[0].Slot;
perCellPlaneCounts[cellId] = maxPlaneCount;
}
var outsideSlicesList = new List(pvFrame.OutsideView.Polygons.Count);
int outsideMaxPlaneCount = 0;
bool outsideHasScissorFallback = false;
foreach (var poly in pvFrame.OutsideView.Polygons)
{
var cps = ClipPlaneSet.From(ViewOf(poly));
if (cps.IsNothingVisible)
continue;
int slot;
Vector4[] planes;
if (cps.Count > 0)
{
planes = ToPlaneSpan(cps);
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));
}
var outsideViewSlices = outsideSlicesList.ToArray();
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;
return new ClipFrameAssembly
{
Frame = frame,
CellIdToSlot = cellIdToSlot,
CellIdToViewSlots = cellIdToViewSlots,
CellIdToViewSlices = cellIdToViewSlices,
OutsideViewSlices = outsideViewSlices,
OutdoorSlot = outdoorSlot,
OutdoorVisible = outdoorVisible,
TerrainMode = terrainMode,
TerrainScissorNdcAabb = terrainScissor,
HasOutsideView = outdoorVisible,
OutsideViewNdcAabb = outsideViewNdcAabb,
OutsidePlaneCount = terrainMode == TerrainClipMode.Planes ? outsideMaxPlaneCount : 0,
PerCellPlaneCounts = perCellPlaneCounts,
ScissorFallbacks = scissorFallbacks,
};
}
private static CellView ViewOf(ViewPolygon poly)
{
var view = new CellView();
view.Add(poly);
return view;
}
private static Vector4 AabbOf(ViewPolygon poly) =>
new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY);
private static int[] ToSlots(ClipViewSlice[] slices)
{
var slots = new int[slices.Length];
for (int i = 0; i < slices.Length; i++)
slots[i] = slices[i].Slot;
return slots;
}
private static Vector4[] ToPlaneSpan(ClipPlaneSet set)
{
int n = set.Count;
var planes = new Vector4[n];
for (int i = 0; i < n; i++)
planes[i] = set.Planes[i];
return planes;
}
}