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>
164 lines
7 KiB
C#
164 lines
7 KiB
C#
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 ~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.
|
||
/// </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.
|
||
}
|
||
}
|