using System.Numerics; using AcDream.App.Rendering; using Xunit; namespace AcDream.App.Tests.Rendering; // Regression tests for the indoor-render HANG (2026-06-06): the portal-visibility flood // re-queues a cell whenever its CellView grows, so it only terminates when CellView.Add's // dedup catches a duplicate. Across BFS rounds the same region comes back float-drifted, // vertex-rotated, or with a ±1 vertex count; the old exact index-by-index SamePolygon // (eps 1e-4) missed all three, so the region grew forever -> CPU-spin hang in CellView.Add. // A drift-tolerant, rotation-invariant dedup makes the key space finite, so the flood // converges (and these duplicates collapse). public class CellViewDedupTests { private static ViewPolygon Quad(float ox, float oy) => new(new[] { new Vector2(ox - 0.5f, oy - 0.5f), new Vector2(ox + 0.5f, oy - 0.5f), new Vector2(ox + 0.5f, oy + 0.5f), new Vector2(ox - 0.5f, oy + 0.5f), }); [Fact] public void Add_DropsSubGridDriftDuplicate() { var v = new CellView(); Assert.True(v.Add(Quad(0f, 0f))); // Same quad, every vertex nudged 3e-4 — beyond the old 1e-4 SamePolygon eps, // within the 1e-3 dedup grid. This is the per-round float drift that caused the hang. var drifted = new ViewPolygon(new[] { new Vector2(-0.5f + 3e-4f, -0.5f - 3e-4f), new Vector2(0.5f + 3e-4f, -0.5f - 3e-4f), new Vector2(0.5f + 3e-4f, 0.5f - 3e-4f), new Vector2(-0.5f + 3e-4f, 0.5f - 3e-4f), }); Assert.False(v.Add(drifted)); Assert.Single(v.Polygons); } [Fact] public void Add_DropsRotatedStartDuplicate() { var v = new CellView(); Assert.True(v.Add(Quad(0f, 0f))); // Same 4 corners (same CCW cycle), but emitted starting at the 2nd vertex — what // Sutherland-Hodgman can do when the subject order differs across rounds. var rotated = new ViewPolygon(new[] { new Vector2(0.5f, -0.5f), new Vector2(0.5f, 0.5f), new Vector2(-0.5f, 0.5f), new Vector2(-0.5f, -0.5f), }); Assert.False(v.Add(rotated)); Assert.Single(v.Polygons); } [Fact] public void Add_KeepsGenuinelyDistinctPolygons() { // The fix must NOT over-merge: two regions 0.4 NDC apart (far beyond the 1e-3 grid) // remain distinct, so a real second portal opening is not silently dropped. var v = new CellView(); Assert.True(v.Add(Quad(0f, 0f))); Assert.True(v.Add(Quad(0.4f, 0f))); Assert.Equal(2, v.Polygons.Count); } [Fact] public void Add_RepeatedDuplicate_DoesNotAllocatePerProbe() { var view = new CellView(); ViewPolygon polygon = Quad(0f, 0f); Assert.True(view.Add(polygon)); // Warm the method/JIT before measuring. Duplicate portal emissions are the hot path. Assert.False(view.Add(polygon)); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 1_000; i++) Assert.False(view.Add(polygon)); long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.True(allocated <= 256, $"duplicate probes allocated {allocated:N0} bytes"); } [Fact] public void ResetAndRebuild_ReusesCanonicalKeyStorage() { var view = new CellView(); ViewPolygon polygon = Quad(0f, 0f); // Warm the retained polygon list, hash table, collision link, and // coordinate payload before measuring frame-to-frame rebuilds. Assert.True(view.Add(polygon)); view.Reset(); Assert.True(view.Add(polygon)); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 1_000; i++) { view.Reset(); Assert.True(view.Add(polygon)); } long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.True(allocated <= 256, $"steady rebuilds allocated {allocated:N0} bytes"); } }