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 // ClipFrameAssembler.cs
// //
// Retail PView assembly policy. PortalVisibilityBuilder produces a retail-like // Retail view assembly policy. The production frame walk supplies its
// view graph: one portal_view list per visible cell plus an outside_view list. // outside_view directly; the legacy PortalVisibilityBuilder overload remains
// This assembler packs each visible polygon as an individual GPU clip slot so // only for isolated research/replay tests. Each visible polygon is packed as
// the renderer can draw the exact PView order: // an individual GPU clip slot.
// //
// outside_view landscape slices // outside_view landscape slices
// reverse cell_draw_list exit masks // reverse cell_draw_list exit masks
@ -40,15 +40,7 @@ public enum TerrainClipMode
public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes); public readonly record struct ClipViewSlice(int Slot, Vector4 NdcAabb, Vector4[] Planes);
/// <summary> /// <summary>
/// Identifies one cell inside one nested building look-in. The same EnvCell can /// Populated clip buffers plus routing data consumed by the render walk.
/// 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.
/// </summary> /// </summary>
public sealed class ClipFrameAssembly public sealed class ClipFrameAssembly
{ {
@ -64,12 +56,6 @@ public sealed class ClipFrameAssembly
/// <summary>Full retail portal_view slices per visible cell.</summary> /// <summary>Full retail portal_view slices per visible cell.</summary>
public Dictionary<uint, ClipViewSlice[]> CellIdToViewSlices { get; } = new(); 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> /// <summary>Full retail outside_view slices.</summary>
public ClipViewSlice[] OutsideViewSlices { get; private set; } = System.Array.Empty<ClipViewSlice>(); public ClipViewSlice[] OutsideViewSlices { get; private set; } = System.Array.Empty<ClipViewSlice>();
@ -106,8 +92,6 @@ public sealed class ClipFrameAssembly
Frame = frame; Frame = frame;
foreach (ClipViewSlice[] slices in CellIdToViewSlices.Values) foreach (ClipViewSlice[] slices in CellIdToViewSlices.Values)
ReturnSlices(slices); ReturnSlices(slices);
foreach (ClipViewSlice[] slices in LookInCellToViewSlices.Values)
ReturnSlices(slices);
foreach (int[] slots in CellIdToViewSlots.Values) foreach (int[] slots in CellIdToViewSlots.Values)
ReturnSlots(slots); ReturnSlots(slots);
if (OutsideViewSlices.Length != 0) if (OutsideViewSlices.Length != 0)
@ -116,8 +100,6 @@ public sealed class ClipFrameAssembly
CellIdToSlot.Clear(); CellIdToSlot.Clear();
CellIdToViewSlots.Clear(); CellIdToViewSlots.Clear();
CellIdToViewSlices.Clear(); CellIdToViewSlices.Clear();
LookInCellToSlot.Clear();
LookInCellToViewSlices.Clear();
PerCellPlaneCounts.Clear(); PerCellPlaneCounts.Clear();
OutsideViewSlices = System.Array.Empty<ClipViewSlice>(); OutsideViewSlices = System.Array.Empty<ClipViewSlice>();
SliceScratch.Clear(); SliceScratch.Clear();
@ -272,6 +254,53 @@ public sealed class ClipFrameAssembly
public static class ClipFrameAssembler 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( public static ClipFrameAssembly Assemble(
ClipFrame frame, ClipFrame frame,
PortalVisibilityFrame pvFrame, PortalVisibilityFrame pvFrame,
@ -529,70 +558,6 @@ public static class ClipFrameAssembler
assembly.ScissorFallbacks = scissorFallbacks; 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) => private static Vector4 AabbOf(ViewPolygon poly) =>
new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY); new(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY);

View file

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

View file

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

View file

@ -275,7 +275,8 @@ internal sealed class WorldRenderDiagnostics
public void EmitPViewInput( public void EmitPViewInput(
bool enabled, bool enabled,
PortalVisibilityFrame portalFrame, IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection, Matrix4x4 viewProjection,
bool outdoorRoot, bool outdoorRoot,
Vector3 eye, Vector3 eye,
@ -294,7 +295,7 @@ internal sealed class WorldRenderDiagnostics
char root = outdoorRoot ? 'Y' : 'n'; char root = outdoorRoot ? 'Y' : 'n';
Matrix4x4 vp = viewProjection; Matrix4x4 vp = viewProjection;
_log.WriteLine(FormattableString.Invariant( _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( public void EmitRetailPViewDiagnostics(
@ -312,8 +313,8 @@ internal sealed class WorldRenderDiagnostics
{ {
AcDream.Core.Rendering.RenderingDiagnostics.EmitVis( AcDream.Core.Rendering.RenderingDiagnostics.EmitVis(
clipRoot.CellId, clipRoot.CellId,
result.PortalFrame.OrderedVisibleCells, result.VisibleCells.OrderBy(static id => id).ToArray(),
result.PortalFrame.OutsideView.Polygons.Count, result.ClipAssembly.OutsideViewSlices.Length,
result.ClipAssembly.OutsidePlaneCount, result.ClipAssembly.OutsidePlaneCount,
result.ClipAssembly.PerCellPlaneCounts, result.ClipAssembly.PerCellPlaneCounts,
result.ClipAssembly.ScissorFallbacks); result.ClipAssembly.ScissorFallbacks);
@ -353,18 +354,12 @@ internal sealed class WorldRenderDiagnostics
bool skyDrawn, bool skyDrawn,
bool depthClear, bool depthClear,
bool outdoorSceneryDrawn, bool outdoorSceneryDrawn,
bool outdoorPortalDrawn,
int outdoorRootObjectCount,
int liveDynamicDrawnCount, int liveDynamicDrawnCount,
string sceneParticles, string sceneParticles,
PortalVisibilityFrame? portalFrame, IReadOnlySet<uint>? visibleCells,
ClipFrameAssembly? clipAssembly, ClipFrameAssembly? clipAssembly,
IReadOnlySet<uint>? drawableCells, IReadOnlySet<uint>? drawableCells,
InteriorEntityPartition.Result? partition, InteriorEntityPartition.Result? partition,
PortalVisibilityFrame? exteriorPortalFrame,
ClipFrameAssembly? exteriorClipAssembly,
IReadOnlySet<uint>? exteriorDrawableCells,
InteriorEntityPartition.Result? exteriorPartition,
Vector3 cameraPosition, Vector3 cameraPosition,
Vector3 playerPosition) Vector3 playerPosition)
{ {
@ -400,7 +395,7 @@ internal sealed class WorldRenderDiagnostics
if (clipAssembly is not null) if (clipAssembly is not null)
{ {
text.Append(" outSlices=").Append(clipAssembly.OutsideViewSlices.Length); 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); text.Append(" outMode=").Append(clipAssembly.TerrainMode);
} }
else else
@ -408,12 +403,11 @@ internal sealed class WorldRenderDiagnostics
text.Append(" outSlices=0 outPolys=0 outMode=none"); 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(" 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(" obj=").Append(FormatPartitionCounts(partition));
text.Append(" outdoorDoor=").Append(outdoorSceneryDrawn ? 'Y' : 'n'); text.Append(" outdoorDoor=").Append(outdoorSceneryDrawn ? 'Y' : 'n');
text.Append(" outdoorRootObjs=").Append(outdoorRootObjectCount);
text.Append(" liveDynDraw=").Append(liveDynamicDrawnCount); text.Append(" liveDynDraw=").Append(liveDynamicDrawnCount);
text.Append(" outRoot=").Append(clipRoot is { IsOutdoorNode: true } ? 'Y' : 'n'); text.Append(" outRoot=").Append(clipRoot is { IsOutdoorNode: true } ? 'Y' : 'n');
if (partition is not null) if (partition is not null)
@ -431,21 +425,6 @@ internal sealed class WorldRenderDiagnostics
text.Append(" bshell=").Append(totalShells).Append('/').Append(shellsWithMeshes); 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(); string signature = text.ToString();
if (signature == _lastRenderSignature) if (signature == _lastRenderSignature)
{ {
@ -506,32 +485,22 @@ internal sealed class WorldRenderDiagnostics
} }
private static string FormatMissingDrawableCells( private static string FormatMissingDrawableCells(
PortalVisibilityFrame? portalFrame, IReadOnlySet<uint>? visibleCells,
IReadOnlySet<uint>? drawableCells) IReadOnlySet<uint>? drawableCells)
{ {
if (portalFrame is null || drawableCells is null) if (visibleCells is null || drawableCells is null)
return "[]"; return "[]";
var text = new StringBuilder(96).Append('['); var text = new StringBuilder(96).Append('[');
int written = 0; int written = 0;
const int MaximumCells = 8; const int MaximumCells = 8;
foreach (uint id in portalFrame.OrderedVisibleCells) foreach (uint id in visibleCells.OrderBy(static id => id))
{ {
if (drawableCells.Contains(id)) if (drawableCells.Contains(id))
continue; continue;
if (written > 0) if (written > 0)
text.Append(','); text.Append(',');
text.Append("0x").Append(id.ToString("X8")); 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) if (++written >= MaximumCells)
{ {
text.Append(",..."); text.Append(",...");

View file

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

View file

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

View file

@ -23,6 +23,48 @@ public class ClipFrameAssemblerTests
return view; return view;
} }
[Fact]
public void BeginWalkFrame_OutdoorRoot_SeedsOneFullScreenDefaultView()
{
using ClipFrame frame = ClipFrame.NoClip();
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(
frame,
outdoorRoot: true);
ClipViewSlice slice = Assert.Single(assembly.OutsideViewSlices);
Assert.Equal(0, slice.Slot);
Assert.Empty(slice.Planes);
Assert.Equal(new Vector4(-1f, -1f, 1f, 1f), slice.NdcAabb);
Assert.True(assembly.OutdoorVisible);
Assert.True(assembly.HasOutsideView);
Assert.Equal(TerrainClipMode.Scissor, assembly.TerrainMode);
Assert.Equal(1, frame.SlotCount);
}
[Fact]
public void BeginWalkFrame_InteriorRoot_ResetsReusedOutdoorAssemblyToEmpty()
{
using ClipFrame frame = ClipFrame.NoClip();
ClipFrameAssembly reuse = ClipFrameAssembler.BeginWalkFrame(
frame,
outdoorRoot: true);
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(
frame,
outdoorRoot: false,
reuse);
Assert.Same(reuse, assembly);
Assert.Empty(assembly.OutsideViewSlices);
Assert.False(assembly.OutdoorVisible);
Assert.False(assembly.HasOutsideView);
Assert.Equal(TerrainClipMode.Skip, assembly.TerrainMode);
Assert.Equal(Vector4.Zero, assembly.OutsideViewNdcAabb);
Assert.Equal(0, assembly.ScissorFallbacks);
Assert.Equal(1, frame.SlotCount);
}
[Fact] [Fact]
public void TwoVisibleCells_PlusOutsideView_ProducesCorrectSlotMapAndCounts() public void TwoVisibleCells_PlusOutsideView_ProducesCorrectSlotMapAndCounts()
{ {
@ -128,39 +170,6 @@ public class ClipFrameAssemblerTests
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode); Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
} }
[Fact]
public void AppendLookInFrames_PacksEveryNestedCellViewWithoutReplacingMainRoutes()
{
const uint mainCell = 0xA9B40100;
const uint lookInCell = 0xA9B40106;
var main = new PortalVisibilityFrame();
main.CellViews[mainCell] = ViewOf(Square(0f, 0f, 0.8f));
main.OrderedVisibleCells.Add(mainCell);
main.OutsideView.Add(Square(0f, 0f, 0.7f));
var nested = new PortalVisibilityFrame();
nested.CellViews[lookInCell] = ViewOf(
Square(-0.35f, 0f, 0.15f),
Square(0.35f, 0f, 0.15f));
nested.OrderedVisibleCells.Add(lookInCell);
using ClipFrame frame = ClipFrame.NoClip();
ClipFrameAssembly assembly = ClipFrameAssembler.Assemble(frame, main);
int mainSlot = assembly.CellIdToSlot[mainCell];
int slotsBeforeLookIn = frame.SlotCount;
ClipFrameAssembler.AppendLookInFrames(frame, [nested], assembly);
var key = new LookInClipCell(0, lookInCell);
ClipViewSlice[] slices = assembly.LookInCellToViewSlices[key];
Assert.Equal(2, slices.Length);
Assert.All(slices, slice => Assert.True(slice.Slot >= slotsBeforeLookIn));
Assert.NotEqual(slices[0].Slot, slices[1].Slot);
Assert.Equal(slices[0].Slot, assembly.LookInCellToSlot[key]);
Assert.Equal(mainSlot, assembly.CellIdToSlot[mainCell]);
Assert.DoesNotContain(lookInCell, assembly.CellIdToSlot.Keys);
}
[Fact] [Fact]
public void Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds() public void Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds()
{ {

View file

@ -1,226 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// Sanctuary cathedral seam captured 2026-08-27. The stationary player is in
/// F4180104 while chase-camera zoom crosses the coincident outside portals at
/// world Y=48 and the viewer root switches between two separate buildings:
/// B3={0103,0104,0105} and B4={0106..0111}. The opposite cathedral half must
/// remain available through the interior-root building look-in on both sides.
/// </summary>
[Trait("Lane", "InstalledDat")]
public sealed class SanctuaryPortalSeamTests
{
private const uint Landblock = 0xF4180000u;
private const uint Cell0104 = Landblock | 0x0104u;
private const uint Cell0106 = Landblock | 0x0106u;
private static Matrix4x4 ViewProjection(Vector3 eye)
{
// Derived from the two exact flap-sweep points. Extending their zoom
// ray reaches the stable chase target at the player's head.
var target = new Vector3(36.033f, 49.638f, 171.353f);
var view = Matrix4x4.CreateLookAt(eye, target, Vector3.UnitZ);
var projection = Matrix4x4.CreatePerspectiveFieldOfView(
1.2f, 893f / 522f, 1f, 5000f);
return view * projection;
}
private static IReadOnlyList<LoadedCell> Cells(
Dictionary<uint, LoadedCell> cells,
uint firstLow,
uint lastLow)
{
var result = new List<LoadedCell>();
for (uint low = firstLow; low <= lastLow; low++)
result.Add(cells[Landblock | low]);
return result;
}
[Fact]
public void CapturedZoomHandoff_BothRootsBuildTheExactOppositeCathedralSeed()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
}
using var dats = new DatCollection(datDir, DatAccessType.Read);
LandBlockInfo info = Assert.IsType<LandBlockInfo>(dats.Get<LandBlockInfo>(Landblock | 0xfffeu));
Dictionary<uint, LoadedCell> cells =
Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
IReadOnlyList<LoadedCell> building3 = Cells(cells, 0x0103u, 0x0105u);
IReadOnlyList<LoadedCell> building4 = Cells(cells, 0x0106u, 0x0111u);
Assert.Equal(0x01001FB2u, info.Buildings[3].ModelId);
Assert.Equal(0x01001FB3u, info.Buildings[4].ModelId);
var captures = new[]
{
new
{
Name = "near/root0104",
Eye = new Vector3(32.742317f, 48.034306f, 172.447845f),
Root = cells[Cell0104],
Opposite = building4,
ExpectedOppositeCell = Cell0106,
ExpectedSeedPortal = 2,
OppositeBuildingIndex = 4,
},
new
{
Name = "far/root0106",
Eye = new Vector3(32.308865f, 47.823200f, 172.592255f),
Root = cells[Cell0106],
Opposite = building3,
ExpectedOppositeCell = Cell0104,
ExpectedSeedPortal = 0,
OppositeBuildingIndex = 3,
},
};
foreach (var capture in captures)
{
Matrix4x4 viewProjection = ViewProjection(capture.Eye);
PortalVisibilityFrame main = PortalVisibilityBuilder.Build(
capture.Root, capture.Eye, Lookup, viewProjection);
PortalVisibilityFrame lookIn = PortalVisibilityBuilder.ConstructViewBuilding(
capture.Opposite,
capture.Eye,
Lookup,
viewProjection,
maxSeedDistance: float.PositiveInfinity,
seedRegion: main.OutsideView.Polygons);
Assert.Contains(capture.ExpectedOppositeCell, lookIn.OrderedVisibleCells);
Assert.True(
lookIn.CellViews.TryGetValue(capture.ExpectedOppositeCell, out CellView? view)
&& view.Polygons.Count > 0,
$"{capture.Name} must retain a clipped aperture for the opposite cathedral half");
ExteriorPortalSeed seed = Assert.Single(lookIn.ExteriorSeedPortals);
Assert.Equal(capture.ExpectedOppositeCell, seed.CellId);
Assert.Equal(capture.ExpectedSeedPortal, seed.PortalIndex);
Assert.NotEmpty(seed.View.Polygons);
using ClipFrame clipFrame = ClipFrame.NoClip();
ClipFrameAssembly assembly = ClipFrameAssembler.Assemble(
clipFrame,
main);
ClipFrameAssembler.AppendLookInFrames(
clipFrame,
[lookIn],
assembly);
ClipViewSlice[] nestedSlices =
assembly.LookInCellToViewSlices[
new LookInClipCell(0, capture.ExpectedOppositeCell)];
Assert.Equal(view.Polygons.Count, nestedSlices.Length);
Assert.All(nestedSlices, slice => Assert.True(slice.Slot > 0));
Assert.Contains(
info.Buildings[capture.OppositeBuildingIndex].Portals,
portal => portal.OtherCellId == (capture.ExpectedOppositeCell & 0xffffu)
&& portal.OtherPortalId == capture.ExpectedSeedPortal);
}
}
[Fact]
public void ReportedLateralTransition_BothRootsKeepTheOppositeFacadePortal()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
Dictionary<uint, LoadedCell> cells =
Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
static (PortalVisibilityFrame Main, PortalVisibilityFrame LookIn) Build(
Vector3 eye,
Vector3 target,
LoadedCell root,
IReadOnlyList<LoadedCell> opposite,
Func<uint, LoadedCell?> lookup)
{
Matrix4x4 viewProjection = Matrix4x4.CreateLookAt(eye, target, Vector3.UnitZ)
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1.6f, 0.1f, 5000f);
PortalVisibilityFrame main = PortalVisibilityBuilder.Build(
root, eye, lookup, viewProjection);
PortalVisibilityFrame lookIn = PortalVisibilityBuilder.ConstructViewBuilding(
opposite, eye, lookup, viewProjection, seedRegion: main.OutsideView.Polygons);
return (main, lookIn);
}
(PortalVisibilityFrame Main, PortalVisibilityFrame LookIn) from0106 = Build(
new Vector3(31.189594f, 46.280170f, 171.783646f),
new Vector3(32.787731f, 46.275948f, 171.354993f),
cells[Cell0106],
Cells(cells, 0x0103u, 0x0105u),
Lookup);
(PortalVisibilityFrame Main, PortalVisibilityFrame LookIn) from0104 = Build(
new Vector3(31.198704f, 49.733287f, 171.783646f),
new Vector3(32.796841f, 49.729065f, 171.354993f),
cells[Cell0104],
Cells(cells, 0x0106u, 0x0111u),
Lookup);
Assert.Equal([Cell0106, Landblock | 0x010Fu], from0106.Main.OrderedVisibleCells);
Assert.Equal([Cell0104], from0106.LookIn.OrderedVisibleCells);
ExteriorPortalSeed seed0104 = Assert.Single(from0106.LookIn.ExteriorSeedPortals);
Assert.Equal(Cell0104, seed0104.CellId);
Assert.Equal(0, seed0104.PortalIndex);
Assert.NotEmpty(seed0104.View.Polygons);
Assert.Equal([Cell0104], from0104.Main.OrderedVisibleCells);
Assert.Equal([Cell0106, Landblock | 0x010Fu], from0104.LookIn.OrderedVisibleCells);
ExteriorPortalSeed seed0106 = Assert.Single(from0104.LookIn.ExteriorSeedPortals);
Assert.Equal(Cell0106, seed0106.CellId);
Assert.Equal(2, seed0106.PortalIndex);
Assert.NotEmpty(seed0106.View.Polygons);
}
[Fact]
public void ReportedExactSteamSeam_Root0104KeepsTheOppositeCathedralPortal()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
Dictionary<uint, LoadedCell> cells =
Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
// Live ACDREAM_PROBE_CELL capture for the user-reported stationary
// frame at player [32.792290, 48.000618, 169.804993]. The chase eye is
// only ~4 mm across the coincident 0104/0106 exterior portal plane.
var eye = new Vector3(31.189665f, 48.004852f, 171.784988f);
var target = new Vector3(32.792290f, 48.000618f, 171.354993f);
Matrix4x4 viewProjection = Matrix4x4.CreateLookAt(eye, target, Vector3.UnitZ)
* Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1555f / 1019f, 1f, 5000f);
PortalVisibilityFrame main = PortalVisibilityBuilder.Build(
cells[Cell0104], eye, Lookup, viewProjection);
PortalVisibilityFrame lookIn = PortalVisibilityBuilder.ConstructViewBuilding(
Cells(cells, 0x0106u, 0x0111u),
eye,
Lookup,
viewProjection,
seedRegion: main.OutsideView.Polygons);
Assert.Contains(Cell0106, lookIn.OrderedVisibleCells);
ExteriorPortalSeed seed = Assert.Single(lookIn.ExteriorSeedPortals);
Assert.Equal(Cell0106, seed.CellId);
Assert.Equal(2, seed.PortalIndex);
Assert.NotEmpty(seed.View.Polygons);
}
}

View file

@ -228,18 +228,12 @@ public sealed class WorldRenderDiagnosticsTests
skyDrawn: false, skyDrawn: false,
depthClear: false, depthClear: false,
outdoorSceneryDrawn: false, outdoorSceneryDrawn: false,
outdoorPortalDrawn: false,
outdoorRootObjectCount: 0,
liveDynamicDrawnCount: 0, liveDynamicDrawnCount: 0,
sceneParticles: "none", sceneParticles: "none",
portalFrame: null, visibleCells: null,
clipAssembly: null, clipAssembly: null,
drawableCells: null, drawableCells: null,
partition: null, partition: null,
exteriorPortalFrame: null,
exteriorClipAssembly: null,
exteriorDrawableCells: null,
exteriorPartition: null,
cameraPosition: Vector3.Zero, cameraPosition: Vector3.Zero,
playerPosition: Vector3.Zero); playerPosition: Vector3.Zero);
} }

View file

@ -739,24 +739,16 @@ public sealed class WorldSceneRendererTests
// Distinct flood-only vs in-view sets: 0x01010003 is a look-in // Distinct flood-only vs in-view sets: 0x01010003 is a look-in
// cell that is drawn but never part of the main flood. // cell that is drawn but never part of the main flood.
_interiorResult = new RetailPViewFrameResult().Reset( _interiorResult = new RetailPViewFrameResult().Reset(
new PortalVisibilityFrame(), ClipFrameAssembler.BeginWalkFrame(
new ClipFrameAssembly(), ClipFrame.NoClip(), outdoorRoot: false),
[0x01010001u], [0x01010001u],
[0x01010001u, 0x01010003u], [0x01010001u, 0x01010003u],
default, default,
default, default,
diagnosticPartition: null); diagnosticPartition: null);
var outdoorPortalFrame = new PortalVisibilityFrame();
outdoorPortalFrame.OutsideView.Add(new ViewPolygon(
[
new Vector2(-1f, -1f),
new Vector2(1f, -1f),
new Vector2(1f, 1f),
new Vector2(-1f, 1f),
]));
_outdoorResult = new RetailPViewFrameResult().Reset( _outdoorResult = new RetailPViewFrameResult().Reset(
outdoorPortalFrame, ClipFrameAssembler.BeginWalkFrame(
ClipFrameAssembler.Assemble(ClipFrame.NoClip(), outdoorPortalFrame), ClipFrame.NoClip(), outdoorRoot: true),
[], [],
[], [],
default, default,
@ -886,7 +878,8 @@ public sealed class WorldSceneRendererTests
public CameraCellResolution CameraCellResolution => CameraCellResolution.None; public CameraCellResolution CameraCellResolution => CameraCellResolution.None;
public void EmitPViewInput( public void EmitPViewInput(
PortalVisibilityFrame portalFrame, IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection, Matrix4x4 viewProjection,
LoadedCell clipRoot, LoadedCell clipRoot,
Vector3 cameraPosition, Vector3 cameraPosition,