acdream/tests/AcDream.App.Tests/Rendering/Issue181VisFlapReplayTests.cs
Erik 05240d2cab feat(render) Campaign FW3.3: ShellDrawLiftZ is RETIRED - cells draw at the dat origin
Retail draws cell geometry at the dat EnvCell origin verbatim; the
0.02 m lift was our z-fight stand-in (register row AP-32, deleted in
this commit). With the walk owning retail draw ORDER under
WorldDepthContract Less (first-drawn-wins: DrawBlock terrain-then-
objects per cell, DrawCells shells-then-contents), the coplanar
tie-breaks the lift compensated for are now resolved the way retail
resolves them.

Deleted at every site: the PortalVisibilityBuilder const + the
drawLiftZ Build parameter and its lifted exit-portal projection branch
(gate and drawn geometry now share ONE space); the seal/punch fan
lifts (DrawPortalDepthWrite + the walk's DrawWalkPunchFan); the
LandblockBuildFactory drawn-cell-transform lift (render and physics
share the one verbatim transform).

The #130 proof flipped exactly as its own doc predicted:
UnliftedGate_LeavesTheStripAtTheDrawnTopEdge is deleted (its premise -
gate space != drawn space - no longer exists), and the renamed
ExitDoorTopEdge_GateCoversTheDrawnApertureWithinPixelTolerance sweep
(147 eye/gaze combos at the Holtburg corner door) passes with both in
the same unlifted space (worst plane gap <= 1.2 px, scissor <= 0.15 px
- unchanged tolerances). Ten more replay-test call sites swept to the
new Build signature.

Suites: full Release build 0 warnings; hermetic 6,750/0; the 21
affected InstalledDat replay tests green; Walk conformance 40/1
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:08:52 +02:00

165 lines
7.2 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>
[Trait("Lane", "InstalledDat")]
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);
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]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_FlappingCellViewRegion_SliverOrLarge()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
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);
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]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_SubMillimeterEyePerturbation_MustNotChangeAdmission()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
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.
}
}