Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the interior seals (live-verified by the user). Commits the session render-rewrite foundation together with the fixes that made it functional. - HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit); only its count is capped. CellViewDedupTests added. - Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey). - Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81 loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled). Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway, confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight): docs/research/2026-06-07-indoor-render-session-handoff.md. Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
224 lines
8 KiB
C#
224 lines
8 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// How the landscape-through-outside_view pass should be interpreted.
|
|
/// </summary>
|
|
public enum TerrainClipMode
|
|
{
|
|
/// <summary>All outside_view slices have convex plane clips.</summary>
|
|
Planes,
|
|
|
|
/// <summary>At least one outside_view slice requires scissor fallback.</summary>
|
|
Scissor,
|
|
|
|
/// <summary>No outside_view slice is visible; skip landscape indoors.</summary>
|
|
Skip,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
|
|
|
|
/// <summary>
|
|
/// Result of <see cref="ClipFrameAssembler.Assemble"/>: populated clip buffers
|
|
/// plus routing data consumed by the render orchestration.
|
|
/// </summary>
|
|
public sealed class ClipFrameAssembly
|
|
{
|
|
public required ClipFrame Frame { get; init; }
|
|
|
|
/// <summary>First drawable slice slot per visible cell. Compatibility map
|
|
/// for renderer APIs that can accept only one slot at a time.</summary>
|
|
public required Dictionary<uint, int> CellIdToSlot { get; init; }
|
|
|
|
/// <summary>Slot-only cell slices, retained for older renderer APIs.</summary>
|
|
public required Dictionary<uint, int[]> CellIdToViewSlots { get; init; }
|
|
|
|
/// <summary>Full retail portal_view slices per visible cell.</summary>
|
|
public required Dictionary<uint, ClipViewSlice[]> CellIdToViewSlices { get; init; }
|
|
|
|
/// <summary>Full retail outside_view slices.</summary>
|
|
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<uint, int> 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<uint, int>();
|
|
var cellIdToViewSlots = new Dictionary<uint, int[]>();
|
|
var cellIdToViewSlices = new Dictionary<uint, ClipViewSlice[]>();
|
|
var perCellPlaneCounts = new Dictionary<uint, int>();
|
|
int scissorFallbacks = 0;
|
|
|
|
foreach (uint cellId in pvFrame.OrderedVisibleCells)
|
|
{
|
|
if (!pvFrame.CellViews.TryGetValue(cellId, out var view))
|
|
continue;
|
|
|
|
var slices = new List<ClipViewSlice>(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<Vector4>();
|
|
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<ClipViewSlice>(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<Vector4>();
|
|
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;
|
|
}
|
|
}
|