using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; namespace AcDream.App.Tests.Rendering; /// /// #181 — the portal-flood admitted set flaps 31↔32 on MICRON eye noise at a /// parked camera (live: launch-176-leakfix.log, root 0x8A020142, vis flips /// every ~100–200 frames for 517k frames; the swept eye carries ~7 µm /// float-roundtrip noise). The flapping cell's visibility-scoped lights + the /// union-AABB scissor rect strobe = the #176 washed-region flicker. /// /// This replay hunts the knife edge headlessly: at the live parked eye /// (49.15, −38.62, −3.98), sweep gazes and perturb the eye by ±0.5 mm per /// axis; ANY admitted-set difference at sub-mm perturbation is the #181 /// instability, and the symmetric difference names the flapping cell. /// public class Issue181VisFlapReplayTests { private const uint FacilityHub = 0x8A020000u; private const uint LiveRoot = FacilityHub | 0x0142u; private static readonly Vector3 LiveEye = new(49.15f, -38.62f, -3.98f); private readonly ITestOutputHelper _out; public Issue181VisFlapReplayTests(ITestOutputHelper output) => _out = output; private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 gazeDir) { var view = Matrix4x4.CreateLookAt(eye, eye + gazeDir, Vector3.UnitZ); var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f); return view * proj; } private static List Flood( Dictionary cells, uint rootId, Vector3 eye, Vector3 gazeDir) { Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; var frame = PortalVisibilityBuilder.Build( cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir), buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ); var result = new List(frame.OrderedVisibleCells); result.Sort(); return result; } private static string CellSetString(IEnumerable ids) => string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}")); [Fact] public void Diagnostic_FlappingCellViewRegion_SliverOrLarge() { var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); uint flapper = FacilityHub | 0x0181u; // The most unstable pose from the perturbation sweep. float yaw = 15f * MathF.PI / 180f, pitch = -20f * MathF.PI / 180f; var gaze = new Vector3( MathF.Cos(pitch) * MathF.Cos(yaw), MathF.Cos(pitch) * MathF.Sin(yaw), MathF.Sin(pitch)); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; foreach (var (label, eye) in new (string, Vector3)[] { ("base ", LiveEye), ("+0.5mm x ", LiveEye + new Vector3(0.0005f, 0, 0)), ("-0.5mm x ", LiveEye - new Vector3(0.0005f, 0, 0)), ("+5mm x ", LiveEye + new Vector3(0.005f, 0, 0)), ("-5mm x ", LiveEye - new Vector3(0.005f, 0, 0)), ("+2cm z ", LiveEye + new Vector3(0, 0, 0.02f)), ("-2cm z ", LiveEye - new Vector3(0, 0, 0.02f)), }) { var frame = PortalVisibilityBuilder.Build( cells[LiveRoot], eye, lookup, ViewProjFor(eye, gaze), buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ); if (!frame.CellViews.TryGetValue(flapper, out var view) || view.IsEmpty) { _out.WriteLine($"{label}: 0181 NOT ADMITTED"); continue; } // NDC shoelace area of the admitted region (screen is 4.0 NDC-units total). float area = 0f; int polys = 0, verts = 0; foreach (var vp in view.Polygons) { var v = vp.Vertices; polys++; verts += v.Length; float a = 0f; for (int i = 0; i < v.Length; i++) { var p = v[i]; var q = v[(i + 1) % v.Length]; a += p.X * q.Y - q.X * p.Y; } area += MathF.Abs(a) * 0.5f; } _out.WriteLine(FormattableString.Invariant( $"{label}: 0181 admitted polys={polys} verts={verts} ndcArea={area:F4} ({area / 4f * 100f:F1}% of screen)")); } } [Fact] public void Diagnostic_SubMillimeterEyePerturbation_MustNotChangeAdmission() { var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); Assert.True(cells.ContainsKey(LiveRoot), "live root 0x0142 not loaded"); // ±0.5 mm per axis — an order of magnitude above the live µm noise, well // below anything visually meaningful. var perturbations = new[] { new Vector3( 0.0005f, 0, 0), new Vector3(-0.0005f, 0, 0), new Vector3(0, 0.0005f, 0), new Vector3(0, -0.0005f, 0), new Vector3(0, 0, 0.0005f), new Vector3(0, 0, -0.0005f), }; int unstableGazes = 0; for (int yawDeg = 0; yawDeg < 360; yawDeg += 15) { foreach (float pitchDeg in new[] { 0f, -20f, -35f }) { float yaw = yawDeg * MathF.PI / 180f, pitch = pitchDeg * MathF.PI / 180f; var gaze = new Vector3( MathF.Cos(pitch) * MathF.Cos(yaw), MathF.Cos(pitch) * MathF.Sin(yaw), MathF.Sin(pitch)); var baseline = Flood(cells, LiveRoot, LiveEye, gaze); foreach (var d in perturbations) { var perturbed = Flood(cells, LiveRoot, LiveEye + d, gaze); if (perturbed.SequenceEqual(baseline)) continue; unstableGazes++; var removed = baseline.Except(perturbed).ToList(); var added = perturbed.Except(baseline).ToList(); _out.WriteLine(FormattableString.Invariant( $"UNSTABLE yaw={yawDeg} pitch={pitchDeg} d=({d.X * 1000:F1},{d.Y * 1000:F1},{d.Z * 1000:F1})mm base={baseline.Count} pert={perturbed.Count} removed=[{CellSetString(removed)}] added=[{CellSetString(added)}]")); } } } _out.WriteLine($"unstable (gaze, perturbation) pairs: {unstableGazes}"); // Diagnostic first: report, don't assert — the fix turns this into a hard pin. } }