refactor(render): remove the production portal frame carrier

This commit is contained in:
Erik 2026-08-31 05:34:46 +02:00
parent 69e69408e8
commit cb22691beb
10 changed files with 128 additions and 443 deletions

View file

@ -1,9 +1,9 @@
// 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:
// Retail view assembly policy. The production frame walk supplies its
// outside_view directly; the legacy PortalVisibilityBuilder overload remains
// only for isolated research/replay tests. Each visible polygon is packed as
// an individual GPU clip slot.
//
// outside_view landscape slices
// reverse cell_draw_list exit masks
@ -40,15 +40,7 @@ public enum TerrainClipMode
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
/// <summary>
/// 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.
/// </summary>
public readonly record struct LookInClipCell(int FrameIndex, uint CellId);
/// <summary>
/// Result of <see cref="ClipFrameAssembler.Assemble"/>: populated clip buffers
/// plus routing data consumed by the render orchestration.
/// Populated clip buffers plus routing data consumed by the render walk.
/// </summary>
public sealed class ClipFrameAssembly
{
@ -64,12 +56,6 @@ public sealed class ClipFrameAssembly
/// <summary>Full retail portal_view slices per visible cell.</summary>
public Dictionary<uint, ClipViewSlice[]> CellIdToViewSlices { get; } = new();
/// <summary>First drawable slice slot per nested look-in cell.</summary>
public Dictionary<LookInClipCell, int> LookInCellToSlot { get; } = new();
/// <summary>All retail portal_view slices per nested look-in cell.</summary>
public Dictionary<LookInClipCell, ClipViewSlice[]> LookInCellToViewSlices { get; } = new();
/// <summary>Full retail outside_view slices.</summary>
public ClipViewSlice[] OutsideViewSlices { get; private set; } = System.Array.Empty<ClipViewSlice>();
@ -106,8 +92,6 @@ public sealed class ClipFrameAssembly
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)
@ -116,8 +100,6 @@ public sealed class ClipFrameAssembly
CellIdToSlot.Clear();
CellIdToViewSlots.Clear();
CellIdToViewSlices.Clear();
LookInCellToSlot.Clear();
LookInCellToViewSlices.Clear();
PerCellPlaneCounts.Clear();
OutsideViewSlices = System.Array.Empty<ClipViewSlice>();
SliceScratch.Clear();
@ -272,6 +254,53 @@ public sealed class ClipFrameAssembly
public static class ClipFrameAssembler
{
/// <summary>
/// Starts one production walk assembly without constructing a parallel
/// PortalVisibilityFrame. Outdoor roots begin with retail's full-screen
/// default view; interior roots begin empty and are populated from
/// <see cref="Walk.RetailFrameWalk.InteriorOutsideView"/> after Collect.
/// </summary>
public static ClipFrameAssembly BeginWalkFrame(
ClipFrame frame,
bool outdoorRoot,
ClipFrameAssembly? reuseAssembly = null)
{
System.ArgumentNullException.ThrowIfNull(frame);
frame.Reset();
ClipFrameAssembly assembly = reuseAssembly ?? new ClipFrameAssembly();
assembly.Reset(frame);
if (outdoorRoot)
{
List<ClipViewSlice> slices = assembly.SliceScratch;
slices.Clear();
var fullScreen = new Vector4(-1f, -1f, 1f, 1f);
slices.Add(new ClipViewSlice(0, fullScreen, System.Array.Empty<Vector4>()));
assembly.SetOutsideViewSlices(assembly.CopySlices(slices));
assembly.OutdoorSlot = 0;
assembly.OutdoorVisible = true;
assembly.TerrainMode = TerrainClipMode.Scissor;
assembly.TerrainScissorNdcAabb = fullScreen;
assembly.HasOutsideView = true;
assembly.OutsideViewNdcAabb = fullScreen;
assembly.OutsidePlaneCount = 0;
assembly.ScissorFallbacks = 1;
}
else
{
assembly.OutdoorSlot = 0;
assembly.OutdoorVisible = false;
assembly.TerrainMode = TerrainClipMode.Skip;
assembly.TerrainScissorNdcAabb = Vector4.Zero;
assembly.HasOutsideView = false;
assembly.OutsideViewNdcAabb = Vector4.Zero;
assembly.OutsidePlaneCount = 0;
assembly.ScissorFallbacks = 0;
}
return assembly;
}
public static ClipFrameAssembly Assemble(
ClipFrame frame,
PortalVisibilityFrame pvFrame,
@ -529,70 +558,6 @@ public static class ClipFrameAssembler
assembly.ScissorFallbacks = scissorFallbacks;
}
/// <summary>
/// Appends the cell views used by nested <c>DrawBuilding -&gt; DrawPortal</c>
/// PViews to the already assembled frame. Retail installs each nested
/// cell's own <c>portal_view</c> before drawing its shell and object list;
/// these slots must therefore be published with the main frame before the
/// first draw is recorded.
/// </summary>
public static void AppendLookInFrames(
ClipFrame frame,
IReadOnlyList<PortalVisibilityFrame> 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<ClipViewSlice> 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<Vector4>();
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);

View file

@ -205,10 +205,10 @@ internal sealed partial class RetailPViewPassExecutor :
}
}
public ClipFrameAssembly AssembleClipFrame(
PortalVisibilityFrame portalFrame,
public ClipFrameAssembly BeginWalkClipFrame(
bool outdoorRoot,
ClipFrameAssembly reuseAssembly) =>
ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
ClipFrameAssembler.BeginWalkFrame(_clipFrame, outdoorRoot, reuseAssembly);
public void PrepareClipFrame(int terrainUploadCount) =>
_surface.PrepareClipFrame(terrainUploadCount);

View file

@ -14,7 +14,6 @@ namespace AcDream.App.Rendering;
internal sealed class RetailPViewRenderer
{
private readonly RenderSceneShadowRuntime _renderSceneShadow;
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
private readonly RetailPViewFrameResult _frameResultScratch = new();
@ -98,19 +97,8 @@ internal sealed class RetailPViewRenderer
RetailPViewPassExecutor walkExecutor = passes as RetailPViewPassExecutor
?? throw new InvalidOperationException(
"The retail frame walk requires RetailPViewPassExecutor.");
// Compatibility carrier only. Production visibility is populated
// exclusively by RetailFrameWalk below; this frame supplies the clip
// assembler's outdoor full-screen seed until that carrier is removed.
PortalVisibilityFrame pvFrame = _mainPortalFrameScratch;
pvFrame.ResetForBuild();
if (ctx.RootCell.IsOutdoorNode)
{
pvFrame.OutsideView.SetFullScreen();
pvFrame.OrderedVisibleCells.Add(ctx.RootCell.CellId);
}
var clipAssembly = passes.AssembleClipFrame(
pvFrame,
ClipFrameAssembly clipAssembly = passes.BeginWalkClipFrame(
ctx.RootCell.IsOutdoorNode,
_clipAssemblyScratch);
// FW4 slice 1: PrepareClipFrame (the one clip-region publication)
// moved BELOW the walk block — an interior-rooted walk frame
@ -123,8 +111,7 @@ internal sealed class RetailPViewRenderer
// (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells).
// Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained).
_drawableCellsScratch.Clear();
_drawableCellsScratch.UnionWith(pvFrame.OrderedVisibleCells);
var drawableCells = _drawableCellsScratch;
HashSet<uint> drawableCells = _drawableCellsScratch;
passes.UseIndoorMembershipOnlyRouting();
// Campaign FW3.4a: THE ONE WALK. Builds walkContext/walkLandscape/
@ -336,7 +323,6 @@ internal sealed class RetailPViewRenderer
RenderFrameDiagnosticCounts counts = WalkDiagnosticCounts(retainedCounts);
RenderProjectionCounts sourceCounts = retainedCounts;
RetailPViewFrameResult result = _frameResultScratch.Reset(
pvFrame,
clipAssembly,
drawableCells,
prepareCells,
@ -695,7 +681,6 @@ public sealed class RetailPViewFrameInput
/// </summary>
public sealed class RetailPViewFrameResult
{
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
public HashSet<uint> DrawableCells { get; private set; } = null!;
@ -712,7 +697,6 @@ public sealed class RetailPViewFrameResult
{ get; private set; }
internal RetailPViewFrameResult Reset(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
HashSet<uint> visibleCells,
@ -720,7 +704,6 @@ public sealed class RetailPViewFrameResult
RenderProjectionCounts sourceCounts,
InteriorEntityPartition.Result? diagnosticPartition)
{
PortalFrame = portalFrame;
ClipAssembly = clipAssembly;
DrawableCells = drawableCells;
VisibleCells = visibleCells;

View file

@ -275,7 +275,8 @@ internal sealed class WorldRenderDiagnostics
public void EmitPViewInput(
bool enabled,
PortalVisibilityFrame portalFrame,
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
bool outdoorRoot,
Vector3 eye,
@ -294,7 +295,7 @@ internal sealed class WorldRenderDiagnostics
char root = outdoorRoot ? 'Y' : 'n';
Matrix4x4 vp = viewProjection;
_log.WriteLine(FormattableString.Invariant(
$"[pv-input] outRoot={root} flood={portalFrame.OrderedVisibleCells.Count} eye=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) player=({player.X:F6},{player.Y:F6},{player.Z:F6}) rawPlayer=({rawPlayer.X:F6},{rawPlayer.Y:F6},{rawPlayer.Z:F6}) yaw={yaw:F8} {terrain} vp=[{vp.M11:F6} {vp.M13:F6} {vp.M22:F6} {vp.M31:F6} {vp.M33:F6} {vp.M41:F6} {vp.M42:F6} {vp.M43:F6}]"));
$"[pv-input] outRoot={root} visible={visibleCells.Count} outsideViews={outsideViewCount} eye=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) player=({player.X:F6},{player.Y:F6},{player.Z:F6}) rawPlayer=({rawPlayer.X:F6},{rawPlayer.Y:F6},{rawPlayer.Z:F6}) yaw={yaw:F8} {terrain} vp=[{vp.M11:F6} {vp.M13:F6} {vp.M22:F6} {vp.M31:F6} {vp.M33:F6} {vp.M41:F6} {vp.M42:F6} {vp.M43:F6}]"));
}
public void EmitRetailPViewDiagnostics(
@ -312,8 +313,8 @@ internal sealed class WorldRenderDiagnostics
{
AcDream.Core.Rendering.RenderingDiagnostics.EmitVis(
clipRoot.CellId,
result.PortalFrame.OrderedVisibleCells,
result.PortalFrame.OutsideView.Polygons.Count,
result.VisibleCells.OrderBy(static id => id).ToArray(),
result.ClipAssembly.OutsideViewSlices.Length,
result.ClipAssembly.OutsidePlaneCount,
result.ClipAssembly.PerCellPlaneCounts,
result.ClipAssembly.ScissorFallbacks);
@ -353,18 +354,12 @@ internal sealed class WorldRenderDiagnostics
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
bool outdoorPortalDrawn,
int outdoorRootObjectCount,
int liveDynamicDrawnCount,
string sceneParticles,
PortalVisibilityFrame? portalFrame,
IReadOnlySet<uint>? visibleCells,
ClipFrameAssembly? clipAssembly,
IReadOnlySet<uint>? drawableCells,
InteriorEntityPartition.Result? partition,
PortalVisibilityFrame? exteriorPortalFrame,
ClipFrameAssembly? exteriorClipAssembly,
IReadOnlySet<uint>? exteriorDrawableCells,
InteriorEntityPartition.Result? exteriorPartition,
Vector3 cameraPosition,
Vector3 playerPosition)
{
@ -400,7 +395,7 @@ internal sealed class WorldRenderDiagnostics
if (clipAssembly is not null)
{
text.Append(" outSlices=").Append(clipAssembly.OutsideViewSlices.Length);
text.Append(" outPolys=").Append(portalFrame?.OutsideView.Polygons.Count ?? 0);
text.Append(" outPolys=").Append(clipAssembly.OutsideViewSlices.Length);
text.Append(" outMode=").Append(clipAssembly.TerrainMode);
}
else
@ -408,12 +403,11 @@ internal sealed class WorldRenderDiagnostics
text.Append(" outSlices=0 outPolys=0 outMode=none");
}
text.Append(" ids=").Append(FormatIds(portalFrame?.OrderedVisibleCells, true));
text.Append(" ids=").Append(FormatIds(visibleCells, false));
text.Append(" draw=").Append(FormatIds(drawableCells, false));
text.Append(" miss=").Append(FormatMissingDrawableCells(portalFrame, drawableCells));
text.Append(" miss=").Append(FormatMissingDrawableCells(visibleCells, drawableCells));
text.Append(" obj=").Append(FormatPartitionCounts(partition));
text.Append(" outdoorDoor=").Append(outdoorSceneryDrawn ? 'Y' : 'n');
text.Append(" outdoorRootObjs=").Append(outdoorRootObjectCount);
text.Append(" liveDynDraw=").Append(liveDynamicDrawnCount);
text.Append(" outRoot=").Append(clipRoot is { IsOutdoorNode: true } ? 'Y' : 'n');
if (partition is not null)
@ -431,21 +425,6 @@ internal sealed class WorldRenderDiagnostics
text.Append(" bshell=").Append(totalShells).Append('/').Append(shellsWithMeshes);
}
if (outdoorPortalDrawn || exteriorPortalFrame is not null
|| exteriorClipAssembly is not null)
{
text.Append(" extPortal=").Append(outdoorPortalDrawn ? 'Y' : 'n');
text.Append(" extSlices=").Append(exteriorClipAssembly?.OutsideViewSlices.Length ?? 0);
text.Append(" extIds=").Append(FormatIds(
exteriorPortalFrame?.OrderedVisibleCells,
true));
text.Append(" extDraw=").Append(FormatIds(exteriorDrawableCells, false));
text.Append(" extMiss=").Append(FormatMissingDrawableCells(
exteriorPortalFrame,
exteriorDrawableCells));
text.Append(" extObj=").Append(FormatPartitionCounts(exteriorPartition));
}
string signature = text.ToString();
if (signature == _lastRenderSignature)
{
@ -506,32 +485,22 @@ internal sealed class WorldRenderDiagnostics
}
private static string FormatMissingDrawableCells(
PortalVisibilityFrame? portalFrame,
IReadOnlySet<uint>? visibleCells,
IReadOnlySet<uint>? drawableCells)
{
if (portalFrame is null || drawableCells is null)
if (visibleCells is null || drawableCells is null)
return "[]";
var text = new StringBuilder(96).Append('[');
int written = 0;
const int MaximumCells = 8;
foreach (uint id in portalFrame.OrderedVisibleCells)
foreach (uint id in visibleCells.OrderBy(static id => id))
{
if (drawableCells.Contains(id))
continue;
if (written > 0)
text.Append(',');
text.Append("0x").Append(id.ToString("X8"));
if (portalFrame.CellViews.TryGetValue(id, out var view))
{
text.Append(":p").Append(view.Polygons.Count);
if (view.IsEmpty)
text.Append(":empty");
}
else
{
text.Append(":noView");
}
if (++written >= MaximumCells)
{
text.Append(",...");

View file

@ -14,7 +14,8 @@ internal interface IWorldSceneDiagnostics
CameraCellResolution CameraCellResolution { get; }
void EmitPViewInput(
PortalVisibilityFrame portalFrame,
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
@ -90,7 +91,8 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
public CameraCellResolution CameraCellResolution => _pview.CameraCellResolution;
public void EmitPViewInput(
PortalVisibilityFrame portalFrame,
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
@ -101,7 +103,8 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
_diagnostics.EmitPViewInput(
enabled: true,
portalFrame,
visibleCells,
outsideViewCount,
viewProjection,
clipRoot.IsOutdoorNode,
cameraPosition,
@ -150,18 +153,12 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
skyDrawn,
depthClear,
outdoorSceneryDrawn,
outdoorPortalDrawn: false,
outdoorRootObjectCount: 0,
liveDynamicDrawnCount,
sceneParticles,
pviewResult?.PortalFrame,
pviewResult?.VisibleCells,
pviewResult?.ClipAssembly,
pviewResult?.DrawableCells,
pviewResult?.DiagnosticPartition,
exteriorPortalFrame: null,
exteriorClipAssembly: null,
exteriorDrawableCells: null,
exteriorPartition: null,
cameraPosition,
playerPosition);
}

View file

@ -262,7 +262,8 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase
_particleVisibility.MarkVisibleCells(pviewResult.VisibleCells);
_frames.ObserveDrawableCells(pviewResult.VisibleCells);
_diagnostics.EmitPViewInput(
pviewResult.PortalFrame,
pviewResult.VisibleCells,
pviewResult.ClipAssembly.OutsideViewSlices.Length,
camera.ViewProjection,
clipRoot,
camera.Position,