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_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. } }