acdream/tests/AcDream.App.Tests/Rendering/Issue95DungeonFloodDiagnosticTests.cs
Erik 95d9dab4bb test(#95): headless dungeon-flood diagnostic — measure visible-cell count on 0x0007
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:52:00 +02:00

198 lines
10 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;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #95 MEASUREMENT (2026-06-13): entering the 0x0007 dungeon (Town Network) explodes
/// WB-DIAG to ~9.1M instances/frame. Suspected cause: <see cref="PortalVisibilityBuilder.Build"/>
/// floods the dungeon's portal graph WITHOUT the retail grab_visible_cells stab_list bounding
/// (decomp:311878). A dungeon cell has <c>seen_outside==0</c>; retail's PVS for it is just the
/// cell's <c>stab_list</c> (<see cref="LoadedCell.VisibleCells"/>) — typically a small bounded
/// set. If our flood instead visits ~all cells of the landblock, that is the blowup.
///
/// This is a DIAGNOSTIC, not a fix: it loads the real 0x0007 interior cells, runs the real
/// production flood from representative dungeon-cell roots, and PRINTS the ground-truth numbers —
/// flood visited-cell-set size (<see cref="PortalVisibilityFrame.OrderedVisibleCells"/>) vs the
/// root's stab_list size (<see cref="LoadedCell.VisibleCells"/>), plus how many visited cells
/// cross landblocks. The single assertion just guarantees the test ran; the VALUE is the output.
/// </summary>
public class Issue95DungeonFloodDiagnosticTests
{
private const uint TownNetwork = 0x00070000u;
private readonly ITestOutputHelper _out;
public Issue95DungeonFloodDiagnosticTests(ITestOutputHelper output) => _out = output;
// Production-ish projection (mirrors the sibling harnesses): FovY ~1.2, 1280x720,
// near 0.1, far 5000. The flood's clip is near-independent, so exactness is not
// load-bearing for cell-count measurement.
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
[Fact]
public void Measure_DungeonFlood_VisibleCellCount()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: dat dir did not resolve (ACDREAM_DAT_DIR unset and "
+ "%USERPROFILE%\\Documents\\Asheron's Call absent). No numbers measured.");
// Diagnostic test: do not hard-fail when dats are absent (matches sibling harnesses).
return;
}
_out.WriteLine($"dat dir resolved: {datDir}");
using var dats = new DatCollection(datDir, DatAccessType.Read);
// 1) LandBlockInfo header — NumCells for 0x0007.
var lbi = dats.Get<DatLandBlockInfo>(TownNetwork | 0xFFFEu);
if (lbi is null)
{
_out.WriteLine($"SKIP: LandBlockInfo 0x{TownNetwork | 0xFFFEu:X8} not found in the dat "
+ "(0x0007 may not exist in this client_cell_1.dat).");
return;
}
_out.WriteLine($"=== 0x0007 (Town Network) LandBlockInfo ===");
_out.WriteLine($"NumCells (DatLandBlockInfo.NumCells) = {lbi.NumCells}");
// 2) Load ALL interior cells (sparse ids tolerated — see LoadAllInteriorCells).
var loaded = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, TownNetwork);
_out.WriteLine($"cells actually loaded = {loaded.Count}");
Assert.True(loaded.Count > 0, "no interior cells loaded for 0x0007 — cannot measure");
Func<uint, LoadedCell?> lookup = id => loaded.TryGetValue(id, out var c) ? c : null;
// 3) Per-cell stab_list (VisibleCells) distribution across ALL loaded cells.
// This is the bounded retail PVS size we expect the flood to roughly match.
var stabSizes = loaded.Values.Select(c => c.VisibleCells.Count).ToList();
int seenOutsideCount = loaded.Values.Count(c => c.SeenOutside);
int interiorCount = loaded.Count - seenOutsideCount;
_out.WriteLine("");
_out.WriteLine("=== stab_list (LoadedCell.VisibleCells) distribution over ALL loaded cells ===");
_out.WriteLine($"cells with SeenOutside==true (entrance/exterior-facing) = {seenOutsideCount}");
_out.WriteLine($"cells with SeenOutside==false (interior dungeon) = {interiorCount}");
if (stabSizes.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"VisibleCells.Count min={stabSizes.Min()} max={stabSizes.Max()} avg={stabSizes.Average():F1} sum={stabSizes.Sum()}"));
int emptyStab = stabSizes.Count(s => s == 0);
_out.WriteLine($"cells with EMPTY stab_list (no dat PVS) = {emptyStab}");
// 4) Pick representative DUNGEON roots: the first interior (SeenOutside==false) cells in
// ascending id order. If none exist, fall back to 0x00070100 and report that.
var interiorRoots = loaded
.Where(kv => !kv.Value.SeenOutside)
.OrderBy(kv => kv.Key)
.Select(kv => kv.Value)
.Take(5)
.ToList();
if (interiorRoots.Count == 0)
{
_out.WriteLine("");
_out.WriteLine("NOTE: NO cell has SeenOutside==false (all cells see the exterior). "
+ "Falling back to root 0x00070100 for the flood measurement.");
if (loaded.TryGetValue(TownNetwork | 0x0100u, out var fallback))
interiorRoots.Add(fallback);
else
{
_out.WriteLine("WARN: 0x00070100 not loaded either; using the lowest-id loaded cell.");
interiorRoots.Add(loaded.OrderBy(kv => kv.Key).First().Value);
}
}
_out.WriteLine("");
_out.WriteLine("=== PER-ROOT FLOOD MEASUREMENT (PortalVisibilityBuilder.Build) ===");
_out.WriteLine("property read for the visited-cell set: PortalVisibilityFrame.OrderedVisibleCells");
_out.WriteLine("root | seenOut | stab(VisibleCells) | flood(OrderedVisibleCells) | crossLB | dir");
var floodSizes = new List<int>();
foreach (var root in interiorRoots)
{
// Eye at the root cell's world origin, looking toward its first portal (or +X if none),
// so the flood actually fires through an opening. Sweep all 6 axis directions and KEEP
// the maximum visited-set — the blowup is a worst-case-over-orientation quantity.
var eye = root.WorldPosition;
int bestFlood = -1;
string bestDir = "?";
int bestCrossLb = -1;
List<uint>? bestVisited = null;
// Direction candidates: toward each portal's polygon centroid (the natural look-through),
// plus the 6 cardinal axes as a fallback sweep.
var lookTargets = new List<(Vector3 target, string label)>();
for (int pi = 0; pi < root.Portals.Count && pi < root.PortalPolygons.Count; pi++)
{
var poly = root.PortalPolygons[pi];
if (poly is { Length: >= 1 })
{
var cl = Vector3.Zero;
foreach (var v in poly) cl += v;
cl /= poly.Length;
lookTargets.Add((Vector3.Transform(cl, root.WorldTransform),
$"portal{pi}->0x{root.Portals[pi].OtherCellId:X4}"));
}
}
foreach (var (d, lbl) in new (Vector3, string)[]
{
(Vector3.UnitX, "+X"), (-Vector3.UnitX, "-X"),
(Vector3.UnitY, "+Y"), (-Vector3.UnitY, "-Y"),
(Vector3.UnitZ, "+Z"), (-Vector3.UnitZ, "-Z"),
})
lookTargets.Add((eye + d * 5f, lbl));
foreach (var (target, label) in lookTargets)
{
if (Vector3.DistanceSquared(target, eye) < 1e-6f) continue;
var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, target));
int floodN = frame.OrderedVisibleCells.Count;
if (floodN > bestFlood)
{
bestFlood = floodN;
bestDir = label;
bestVisited = frame.OrderedVisibleCells;
bestCrossLb = frame.OrderedVisibleCells.Count(id => (id & 0xFFFF0000u) != TownNetwork);
}
}
floodSizes.Add(bestFlood);
_out.WriteLine(FormattableString.Invariant(
$"0x{root.CellId:X8} | {(root.SeenOutside ? "Y" : "N"),5} | {root.VisibleCells.Count,18} | {bestFlood,26} | {bestCrossLb,7} | {bestDir}"));
// For the FIRST root, also print the actual visited set + stab set for eyeballing.
if (ReferenceEquals(root, interiorRoots[0]) && bestVisited is not null)
{
_out.WriteLine(" first-root visited (OrderedVisibleCells, low ids): "
+ string.Join(" ", bestVisited.Select(id => $"{id & 0xFFFFu:X4}")));
_out.WriteLine(" first-root stab_list (VisibleCells, low ids): "
+ string.Join(" ", root.VisibleCells.Select(id => $"{id & 0xFFFFu:X4}")));
}
}
// 5) Aggregate flood-size stats across the sampled roots — the headline numbers.
_out.WriteLine("");
_out.WriteLine("=== AGGREGATE over sampled roots ===");
if (floodSizes.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"flood visited-set size (OrderedVisibleCells): min={floodSizes.Min()} max={floodSizes.Max()} avg={floodSizes.Average():F1} (NumCells={lbi.NumCells}, loaded={loaded.Count})"));
var sampledStab = interiorRoots.Select(r => r.VisibleCells.Count).ToList();
if (sampledStab.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"sampled roots' stab_list size (VisibleCells): min={sampledStab.Min()} max={sampledStab.Max()} avg={sampledStab.Average():F1}"));
_out.WriteLine("");
_out.WriteLine("INTERPRETATION: if flood max ~= loaded.Count (visits ~all cells) while stab "
+ "is small, that is the #95 blowup — the flood is unbounded by the retail stab_list PVS.");
}
}