acdream/tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs
Erik 1405dd8e90 feat(render): indoor render WORKS — terminating portal flood + every-cell seal + look-in FPS
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>
2026-06-07 10:14:43 +02:00

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);
}
}