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>
209 lines
7.7 KiB
C#
209 lines
7.7 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public class ClipFrameAssemblerTests
|
|
{
|
|
private static ViewPolygon Square(float cx, float cy, float half) => new(new[]
|
|
{
|
|
new Vector2(cx - half, cy - half),
|
|
new Vector2(cx + half, cy - half),
|
|
new Vector2(cx + half, cy + half),
|
|
new Vector2(cx - half, cy + half),
|
|
});
|
|
|
|
private static CellView ViewOf(params ViewPolygon[] polys)
|
|
{
|
|
var view = new CellView();
|
|
foreach (var p in polys)
|
|
view.Add(p);
|
|
return view;
|
|
}
|
|
|
|
[Fact]
|
|
public void TwoVisibleCells_PlusOutsideView_ProducesCorrectSlotMapAndCounts()
|
|
{
|
|
const uint cellA = 0xA9B40100;
|
|
const uint cellB = 0xA9B40101;
|
|
|
|
var pv = new PortalVisibilityFrame();
|
|
pv.CellViews[cellA] = ViewOf(Square(-0.3f, 0f, 0.3f));
|
|
pv.CellViews[cellB] = ViewOf(Square(0.3f, 0f, 0.2f));
|
|
pv.OrderedVisibleCells.Add(cellA);
|
|
pv.OrderedVisibleCells.Add(cellB);
|
|
pv.OutsideView.Add(Square(0f, 0.5f, 0.25f));
|
|
|
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
|
|
|
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + two cells + one outside slice
|
|
Assert.Contains(cellA, asm.CellIdToSlot.Keys);
|
|
Assert.Contains(cellB, asm.CellIdToSlot.Keys);
|
|
Assert.NotEqual(0, asm.CellIdToSlot[cellA]);
|
|
Assert.NotEqual(0, asm.CellIdToSlot[cellB]);
|
|
Assert.NotEqual(asm.CellIdToSlot[cellA], asm.CellIdToSlot[cellB]);
|
|
Assert.Equal(4, asm.PerCellPlaneCounts[cellA]);
|
|
Assert.Equal(4, asm.PerCellPlaneCounts[cellB]);
|
|
|
|
Assert.True(asm.OutdoorVisible);
|
|
Assert.NotEqual(0, asm.OutdoorSlot);
|
|
Assert.Single(asm.OutsideViewSlices);
|
|
Assert.Equal(asm.OutdoorSlot, asm.OutsideViewSlices[0].Slot);
|
|
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
|
|
Assert.Equal(4, asm.OutsidePlaneCount);
|
|
Assert.Equal(0, asm.ScissorFallbacks);
|
|
}
|
|
|
|
[Fact]
|
|
public void NothingVisibleCell_IsExcludedFromSlotMap_AndNotAppended()
|
|
{
|
|
const uint cellA = 0xA9B40100;
|
|
const uint cellB = 0xA9B40101;
|
|
|
|
var pv = new PortalVisibilityFrame();
|
|
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
|
|
pv.CellViews[cellB] = new CellView();
|
|
pv.OrderedVisibleCells.Add(cellA);
|
|
pv.OrderedVisibleCells.Add(cellB);
|
|
|
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
|
|
|
Assert.Equal(2, asm.Frame.SlotCount);
|
|
Assert.Contains(cellA, asm.CellIdToSlot.Keys);
|
|
Assert.DoesNotContain(cellB, asm.CellIdToSlot.Keys);
|
|
Assert.False(asm.OutdoorVisible);
|
|
Assert.Empty(asm.OutsideViewSlices);
|
|
Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode);
|
|
Assert.Equal(0, asm.OutsidePlaneCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void OutsideViewMultiPolygon_PreservesRetailSlices()
|
|
{
|
|
const uint cellA = 0xA9B40100;
|
|
|
|
var pv = new PortalVisibilityFrame();
|
|
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
|
|
pv.OrderedVisibleCells.Add(cellA);
|
|
pv.OutsideView.Add(Square(-0.5f, 0f, 0.1f));
|
|
pv.OutsideView.Add(Square(0.5f, 0f, 0.1f));
|
|
|
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
|
|
|
Assert.True(asm.OutdoorVisible);
|
|
Assert.NotEqual(0, asm.OutdoorSlot);
|
|
Assert.Equal(2, asm.OutsideViewSlices.Length);
|
|
Assert.NotEqual(asm.OutsideViewSlices[0].Slot, asm.OutsideViewSlices[1].Slot);
|
|
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
|
|
Assert.Equal(4, asm.OutsidePlaneCount);
|
|
Assert.Equal(0, asm.ScissorFallbacks);
|
|
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + cell + two outside slices
|
|
Assert.Equal(Vector4.Zero, asm.TerrainScissorNdcAabb);
|
|
}
|
|
|
|
[Fact]
|
|
public void CellMultiPolygonView_PreservesRetailViewSlices()
|
|
{
|
|
const uint cellA = 0xA9B40100;
|
|
|
|
var pv = new PortalVisibilityFrame();
|
|
pv.CellViews[cellA] = ViewOf(
|
|
Square(-0.4f, 0f, 0.1f),
|
|
Square(0.4f, 0f, 0.1f));
|
|
pv.OrderedVisibleCells.Add(cellA);
|
|
pv.OutsideView.Add(Square(0f, 0f, 0.3f));
|
|
|
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
|
|
|
Assert.True(asm.CellIdToSlot[cellA] > 0);
|
|
Assert.Equal(2, asm.CellIdToViewSlots[cellA].Length);
|
|
Assert.Equal(2, asm.CellIdToViewSlices[cellA].Length);
|
|
Assert.NotEqual(asm.CellIdToViewSlots[cellA][0], asm.CellIdToViewSlots[cellA][1]);
|
|
Assert.Equal(4, asm.PerCellPlaneCounts[cellA]);
|
|
Assert.Single(asm.OutsideViewSlices);
|
|
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + two cell slices + outside slice
|
|
Assert.Equal(0, asm.ScissorFallbacks);
|
|
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
|
|
}
|
|
|
|
[Fact]
|
|
public void Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds()
|
|
{
|
|
var pv = new PortalVisibilityFrame();
|
|
pv.OutsideView.Add(Square(-0.3f, 0.2f, 0.25f));
|
|
|
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
|
|
|
Assert.True(asm.HasOutsideView);
|
|
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
|
|
Assert.Single(asm.OutsideViewSlices);
|
|
|
|
var expected = new Vector4(
|
|
pv.OutsideView.MinX, pv.OutsideView.MinY,
|
|
pv.OutsideView.MaxX, pv.OutsideView.MaxY);
|
|
Assert.Equal(expected, asm.OutsideViewNdcAabb);
|
|
}
|
|
|
|
[Fact]
|
|
public void Assemble_OutsideViewMultiPolygon_PreservesSlicesAndUnionAabb()
|
|
{
|
|
var pv = new PortalVisibilityFrame();
|
|
pv.OutsideView.Add(Square(-0.6f, 0f, 0.15f));
|
|
pv.OutsideView.Add(Square(0.6f, 0f, 0.15f));
|
|
|
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
|
|
|
Assert.True(asm.HasOutsideView);
|
|
Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode);
|
|
Assert.Equal(2, asm.OutsideViewSlices.Length);
|
|
|
|
var expected = new Vector4(
|
|
pv.OutsideView.MinX, pv.OutsideView.MinY,
|
|
pv.OutsideView.MaxX, pv.OutsideView.MaxY);
|
|
Assert.Equal(expected, asm.OutsideViewNdcAabb);
|
|
Assert.Equal(Vector4.Zero, asm.TerrainScissorNdcAabb);
|
|
}
|
|
|
|
[Fact]
|
|
public void Assemble_EmptyOutsideView_HasOutsideViewFalse_AabbZero()
|
|
{
|
|
const uint cellA = 0xA9B40100;
|
|
var pv = new PortalVisibilityFrame();
|
|
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
|
|
pv.OrderedVisibleCells.Add(cellA);
|
|
|
|
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
|
|
|
|
Assert.False(asm.HasOutsideView);
|
|
Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode);
|
|
Assert.Equal(Vector4.Zero, asm.OutsideViewNdcAabb);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ReusesFrame_NoSlotLeakAcrossAssemblies()
|
|
{
|
|
var frame = ClipFrame.NoClip();
|
|
|
|
var pv1 = new PortalVisibilityFrame();
|
|
pv1.CellViews[0xA9B40100] = ViewOf(Square(-0.3f, 0f, 0.2f));
|
|
pv1.CellViews[0xA9B40101] = ViewOf(Square(0.3f, 0f, 0.2f));
|
|
pv1.OrderedVisibleCells.Add(0xA9B40100);
|
|
pv1.OrderedVisibleCells.Add(0xA9B40101);
|
|
pv1.OutsideView.Add(Square(0f, 0.4f, 0.2f));
|
|
var asm1 = ClipFrameAssembler.Assemble(frame, pv1);
|
|
Assert.Equal(4, asm1.Frame.SlotCount);
|
|
|
|
var pv2 = new PortalVisibilityFrame();
|
|
pv2.CellViews[0xA9B40200] = ViewOf(Square(0f, 0f, 0.25f));
|
|
pv2.OrderedVisibleCells.Add(0xA9B40200);
|
|
var asm2 = ClipFrameAssembler.Assemble(frame, pv2);
|
|
|
|
Assert.Equal(2, asm2.Frame.SlotCount);
|
|
Assert.Contains(0xA9B40200, asm2.CellIdToSlot.Keys);
|
|
Assert.DoesNotContain(0xA9B40100, asm2.CellIdToSlot.Keys);
|
|
Assert.False(asm2.OutdoorVisible);
|
|
Assert.Equal(TerrainClipMode.Skip, asm2.TerrainMode);
|
|
}
|
|
}
|