using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// Phase U.4: GL-free proof that implements the /// slot/gate policy — slot 0 no-clip, one CellClip slot per visible cell with a /// convex region, OutsideView routed to the terrain decision + the outdoor mesh /// slot, and the three Count==0 dispositions (nothing-visible cull, scissor /// fallback → no-clip, planes). Hand-built s /// drive the assembler directly (no portal BFS needed) so each disposition is /// exercised in isolation. /// public class ClipFrameAssemblerTests { // A convex NDC square → ClipPlaneSet.From yields 4 planes (Count > 0). 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 v = new CellView(); foreach (var p in polys) v.Add(p); return v; } [Fact] public void TwoVisibleCells_PlusOutsideView_ProducesCorrectSlotMapAndCounts() { // Two cells with single convex regions (→ planes, mapped to slots 1 and 2) // and a single-convex OutsideView (→ planes, the outdoor slot 3). 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 frame = ClipFrame.NoClip(); var asm = ClipFrameAssembler.Assemble(frame, pv); // slot 0 reserved (no-clip) + 2 cells + 1 outdoor = 4 slots. Assert.Equal(4, asm.Frame.SlotCount); // Both cells mapped to NON-zero slots (real plane regions), distinct. Assert.True(asm.CellIdToSlot.ContainsKey(cellA)); Assert.True(asm.CellIdToSlot.ContainsKey(cellB)); Assert.NotEqual(0, asm.CellIdToSlot[cellA]); Assert.NotEqual(0, asm.CellIdToSlot[cellB]); Assert.NotEqual(asm.CellIdToSlot[cellA], asm.CellIdToSlot[cellB]); // Per-cell plane counts recorded (a convex square reduces to 4 planes). Assert.Equal(4, asm.PerCellPlaneCounts[cellA]); Assert.Equal(4, asm.PerCellPlaneCounts[cellB]); // OutsideView: visible, convex → planes, a non-zero outdoor slot, terrain // gated via planes. Assert.True(asm.OutdoorVisible); Assert.NotEqual(0, asm.OutdoorSlot); Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode); Assert.Equal(4, asm.OutsidePlaneCount); Assert.Equal(0, asm.ScissorFallbacks); // The outdoor slot differs from both cell slots and from slot 0. Assert.NotEqual(asm.CellIdToSlot[cellA], asm.OutdoorSlot); Assert.NotEqual(asm.CellIdToSlot[cellB], asm.OutdoorSlot); } [Fact] public void NothingVisibleCell_IsExcludedFromSlotMap_AndNotAppended() { // cellA is a real convex region; cellB's CellView is EMPTY → ClipPlaneSet // .IsNothingVisible → it must NOT be mapped and NOT consume a slot. 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(); // empty ⇒ nothing visible pv.OrderedVisibleCells.Add(cellA); pv.OrderedVisibleCells.Add(cellB); // OutsideView empty ⇒ terrain Skip (the bleed fix), outdoor not visible. var frame = ClipFrame.NoClip(); var asm = ClipFrameAssembler.Assemble(frame, pv); // slot 0 + cellA only = 2 slots. cellB consumed none. Assert.Equal(2, asm.Frame.SlotCount); Assert.True(asm.CellIdToSlot.ContainsKey(cellA)); Assert.False(asm.CellIdToSlot.ContainsKey(cellB)); // culled — not drawable // Empty OutsideView ⇒ outdoor culled + terrain skipped (the bleed fix). Assert.False(asm.OutdoorVisible); Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode); Assert.Equal(0, asm.OutsidePlaneCount); } [Fact] public void OutsideViewScissorFallback_MapsTerrainToScissor_AndCountsFallback() { // A MULTI-polygon OutsideView is a union (non-convex) ⇒ ClipPlaneSet falls // back to the union AABB scissor: mesh outdoor slot 0 (no-clip over-include), // terrain → Scissor, one fallback counted. 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)); // 2 polys ⇒ scissor fallback var frame = ClipFrame.NoClip(); var asm = ClipFrameAssembler.Assemble(frame, pv); Assert.True(asm.OutdoorVisible); Assert.Equal(0, asm.OutdoorSlot); // no-clip over-include Assert.Equal(TerrainClipMode.Scissor, asm.TerrainMode); Assert.Equal(0, asm.OutsidePlaneCount); // scissor ⇒ 0 planes Assert.Equal(1, asm.ScissorFallbacks); // The terrain scissor AABB is a valid (min <= max) NDC box spanning both // OutsideView squares: minX <= -0.6, maxX >= 0.6. Assert.True(asm.TerrainScissorNdcAabb.X <= asm.TerrainScissorNdcAabb.Z); Assert.True(asm.TerrainScissorNdcAabb.Y <= asm.TerrainScissorNdcAabb.W); Assert.True(asm.TerrainScissorNdcAabb.X <= -0.59f); Assert.True(asm.TerrainScissorNdcAabb.Z >= 0.59f); } [Fact] public void CellScissorFallback_MapsCellToSlotZero_AndCountsFallback() { // A cell with a MULTI-polygon region → scissor fallback → mapped to slot 0 // (no-clip over-include), recorded with 0 planes, one fallback counted. The // OutsideView is a single convex region (planes) so only the CELL counts. 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 frame = ClipFrame.NoClip(); var asm = ClipFrameAssembler.Assemble(frame, pv); // cellA → slot 0 (no-clip). slot 0 + the OutsideView's planes slot = 2. Assert.Equal(0, asm.CellIdToSlot[cellA]); Assert.Equal(0, asm.PerCellPlaneCounts[cellA]); Assert.Equal(2, asm.Frame.SlotCount); // slot0 + OutsideView Assert.Equal(1, asm.ScissorFallbacks); // the cell fallback Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode); } // ----------------------------------------------------------------------- // Phase W Stage 4: HasOutsideView + OutsideViewNdcAabb // ----------------------------------------------------------------------- [Fact] public void Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds() { // A single convex quad in OutsideView reduces to Planes. HasOutsideView must // be true and OutsideViewNdcAabb must match the polygon's own Min/Max values. var pv = new PortalVisibilityFrame(); var poly = Square(-0.3f, 0.2f, 0.25f); pv.OutsideView.Add(poly); // No interior cells needed for this assertion. var frame = ClipFrame.NoClip(); var asm = ClipFrameAssembler.Assemble(frame, pv); Assert.True(asm.HasOutsideView); Assert.Equal(TerrainClipMode.Planes, asm.TerrainMode); // The OutsideViewNdcAabb must equal the CellView's accumulated Min/Max. var expected = new System.Numerics.Vector4( pv.OutsideView.MinX, pv.OutsideView.MinY, pv.OutsideView.MaxX, pv.OutsideView.MaxY); Assert.Equal(expected, asm.OutsideViewNdcAabb); } [Fact] public void Assemble_OutsideViewMultiPolygon_ScissorMode_HasOutsideViewTrue_AabbValid() { // Two polygons in OutsideView → union (non-convex) → ClipPlaneSet forces // scissor fallback. HasOutsideView must still be true, TerrainMode must be // Scissor, and OutsideViewNdcAabb must equal the union bounds (same values // as TerrainScissorNdcAabb in this mode). var pv = new PortalVisibilityFrame(); pv.OutsideView.Add(Square(-0.6f, 0f, 0.15f)); pv.OutsideView.Add(Square(0.6f, 0f, 0.15f)); var frame = ClipFrame.NoClip(); var asm = ClipFrameAssembler.Assemble(frame, pv); Assert.True(asm.HasOutsideView); Assert.Equal(TerrainClipMode.Scissor, asm.TerrainMode); // Union bounds from the CellView (spans both squares). var expectedAabb = new System.Numerics.Vector4( pv.OutsideView.MinX, pv.OutsideView.MinY, pv.OutsideView.MaxX, pv.OutsideView.MaxY); Assert.Equal(expectedAabb, asm.OutsideViewNdcAabb); // In Scissor mode OutsideViewNdcAabb and TerrainScissorNdcAabb are the same // value (both are the union CellView bounds). Assert.Equal(asm.TerrainScissorNdcAabb, asm.OutsideViewNdcAabb); } [Fact] public void Assemble_EmptyOutsideView_HasOutsideViewFalse_AabbZero() { // An empty OutsideView means no exit portal was in view → TerrainMode.Skip, // HasOutsideView false, OutsideViewNdcAabb degenerate zero. const uint cellA = 0xA9B40100; var pv = new PortalVisibilityFrame(); pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f)); pv.OrderedVisibleCells.Add(cellA); // OutsideView left empty (no exit portal). var frame = ClipFrame.NoClip(); var asm = ClipFrameAssembler.Assemble(frame, pv); Assert.False(asm.HasOutsideView); Assert.Equal(TerrainClipMode.Skip, asm.TerrainMode); Assert.Equal(System.Numerics.Vector4.Zero, asm.OutsideViewNdcAabb); } [Fact] public void Reset_ReusesFrame_NoSlotLeakAcrossAssemblies() { // First assembly packs 2 cells + outdoor (4 slots). Re-assembling the SAME // frame from a smaller pvFrame must Reset back to slot 0 — no leak. 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); // Second assembly: a single cell, no OutsideView. var pv2 = new PortalVisibilityFrame(); pv2.CellViews[0xA9B40200] = ViewOf(Square(0f, 0f, 0.25f)); pv2.OrderedVisibleCells.Add(0xA9B40200); var asm2 = ClipFrameAssembler.Assemble(frame, pv2); // slot 0 + 1 cell = 2 — the prior 4-slot state did not leak. Assert.Equal(2, asm2.Frame.SlotCount); Assert.True(asm2.CellIdToSlot.ContainsKey(0xA9B40200)); Assert.False(asm2.CellIdToSlot.ContainsKey(0xA9B40100)); // gone after Reset Assert.False(asm2.OutdoorVisible); // no OutsideView this time Assert.Equal(TerrainClipMode.Skip, asm2.TerrainMode); } }