acdream/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs
Erik d366f36557 diag #181 (refined): 0x0181's region is a zero-area sliver; the amplifier is per-drawn-cell state shifting with the cell list, not light-pool scoping
Diagnostic_FlappingCellViewRegion_SliverOrLarge: at the flap pose the admitted region is one degenerate triangle (ndcArea ~0), and ClipPlaneSet.From handles degenerates correctly (area<1e-7 -> Empty) - the cell's own gated geometry costs ~zero pixels either way. The a7 pseudocode CORRECTION-2 re-read kills the flood-scoped-lights framing (the pool is already resident+player-anchored since d8984e87). Remaining suspects: per-cell light-set SSBO slot assignment (SelectForCell) or the seal/punch assembly keyed to the drawn-cell list. Next instrument: parked ACDREAM_PROBE_SEAMDRAW=1 run, diff the washed cell's [seam-blk] applied-set lines between vis=31 and vis=32 frames. ISSUES #181 + render digest updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:46:49 +02:00

164 lines
7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// #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 ~100200 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.
/// </summary>
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<uint> Flood(
Dictionary<uint, LoadedCell> cells, uint rootId, Vector3 eye, Vector3 gazeDir)
{
Func<uint, LoadedCell?> 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<uint>(frame.OrderedVisibleCells);
result.Sort();
return result;
}
private static string CellSetString(IEnumerable<uint> 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<uint, LoadedCell?> 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.
}
}