test(#95): headless dungeon-flood diagnostic — measure visible-cell count on 0x0007
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd7b73a837
commit
95d9dab4bb
1 changed files with 198 additions and 0 deletions
|
|
@ -0,0 +1,198 @@
|
|||
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.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue