docs+test #176/#177: 12 mechanisms refuted, apparatus shipped, probe protocol staged

The dungeon render pair (purple seam flash + stair pop-in) resisted
desk root-causing, but the investigation narrowed the space to two
live theories and shipped permanent apparatus:

- Issue176177DungeonSeamInspectionTests: dat truth — corridor floors
  ARE textured drawn PortalSide portal polys; the reciprocal is NoPos;
  the 'stairs' are 0x8A020182's ramp shell (vertical portals, zero
  statics); CellBSP partitions exactly at portal planes; DXT1 textures
  carry zero transparent-mode texels.
- Issue176177FacilityHubFloodReplayTests: production-matched flood
  replays — approach/descent/gaze-sweep/walk all healthy with coherent
  inputs; ScenarioE pins the flood's collapse-to-root sensitivity under
  incoherent (root, eye) pairs.
- Issue176177SeamTransitLagTests: the resolver flips cells within one
  tick-step of the portal plane — the 0.33-0.47m [cell-transit] 'lag'
  in the gate logs is speed*tick quantization, not membership error.

Refuted (do NOT retry — ledger in the research doc): placeholder
texture, reciprocal z-fight, seal z-fight (seals only fire for
OtherCellId==0xFFFF), root/eye incoherence (production camera sweep is
mm-exact at planes), flood bistability, #119-class statics, undefined
DXT mips (both paths decode DXT->RGBA8; the compressed-array branch is
dead code), DXT1 alpha, fog mix (ramp ~538m), lightning leak (flash==0
in production), viewer-light pops (smooth (1-d/range) ramp).

Filed #178 (A8 double-sided shell stopgap still live) and #179
(lightning flash lacks an indoor gate — dormant). The purple can only
be the fog clear color (undrawn pixels) or the outdoor ambient+sun
tint; discrimination needs ONE probe launch (ACDREAM_PROBE_LIGHT +
ACDREAM_PROBE_PVINPUT + ACDREAM_PROBE_CELL) — protocol in
docs/research/2026-07-06-176-177-render-pair-investigation.md.

Suites: Core 2587 / App 719 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-05 21:20:03 +02:00
parent 544d4d3eef
commit b8e9e204ad
5 changed files with 1176 additions and 19 deletions

View file

@ -0,0 +1,157 @@
using System;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using Env = System.Environment;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// #176/#177 membership half: production [cell-transit] lines
/// (launch-137-gate2.log) fire 0.10.6 m PAST the portal plane in the travel
/// direction (016E→017A at x=85.3385.47 vs the plane at x=85.00), while the
/// dat CellBSP volumes partition EXACTLY at the plane
/// (Issue176177DungeonSeamInspectionTests.SeamCells_CellBspContainment) —
/// retail's center-only point_in_cell flips at the plane. The render root
/// (viewer cell) resolves through the same machinery; while it lags, the
/// portal flood correctly refuses the boundary portal the eye has already
/// crossed and the whole forward chain drops (the purple seam flash /
/// stair pop). This replay measures OUR resolver's flip point across the
/// x=85 seam in a controlled run.
/// </summary>
public class Issue176177SeamTransitLagTests
{
private const uint SeamCellWest = 0x8A02016Eu; // x 75..85
private const uint SeamCellEast = 0x8A02017Au; // x 85..88.33
private readonly ITestOutputHelper _out;
public Issue176177SeamTransitLagTests(ITestOutputHelper output) => _out = output;
private static string? ResolveDatDir()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(datDir) ? datDir : null;
}
private static PhysicsEngine BuildEngine(DatCollection dats)
{
var engine = new PhysicsEngine();
engine.DataCache = new PhysicsDataCache();
var toLoad = new System.Collections.Generic.HashSet<uint> { SeamCellWest, SeamCellEast };
for (int ring = 0; ring < 3; ring++)
{
foreach (var known in new System.Collections.Generic.List<uint>(toLoad))
{
var cell = dats.Get<EnvCell>(known);
if (cell is null) continue;
foreach (var p in cell.CellPortals)
toLoad.Add(0x8A020000u | p.OtherCellId);
}
}
foreach (var cellId in toLoad)
{
var envCell = dats.Get<EnvCell>(cellId);
if (envCell is null) continue;
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(0x0D000000u | envCell.EnvironmentId);
if (environment is null) continue;
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cs)) continue;
var rot = new Quaternion(
envCell.Position.Orientation.X, envCell.Position.Orientation.Y,
envCell.Position.Orientation.Z, envCell.Position.Orientation.W);
var world = Matrix4x4.CreateFromQuaternion(rot)
* Matrix4x4.CreateTranslation(
envCell.Position.Origin.X, envCell.Position.Origin.Y, envCell.Position.Origin.Z);
engine.DataCache.CacheCellStruct(cellId, envCell, cs!, world);
}
return engine;
}
private static PhysicsBody GroundedBody()
{
var body = new PhysicsBody();
body.ContactPlaneValid = true;
body.ContactPlane = new Plane(Vector3.UnitZ, 6f);
body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
body.WalkablePolygonValid = true;
body.WalkablePlane = new Plane(Vector3.UnitZ, 6f);
body.WalkableUp = Vector3.UnitZ;
body.WalkableVertices = new[]
{
new Vector3(75f, -41.67f, -6f),
new Vector3(85f, -41.67f, -6f),
new Vector3(85f, -38.33f, -6f),
new Vector3(75f, -38.33f, -6f),
};
return body;
}
/// <summary>
/// Run +X across the x=85 seam at run-speed tick steps (13.5 cm/tick ≈
/// 4 m/s at 30 Hz) and record where ResolveWithTransition's CellId flips.
/// Retail (center-only point_in_cell, exact-partition CellBSP) flips on
/// the first tick whose END position is past x=85.00 — any flip later
/// than one step past the plane is OUR lag.
/// </summary>
[Theory]
[InlineData(+1)] // west → east across x=85
[InlineData(-1)] // east → west back across
public void RunAcrossSeam_CellFlipPosition(int direction)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var engine = BuildEngine(dats);
var body = GroundedBody();
const float step = 0.135f;
float startX = direction > 0 ? 83.8f : 86.2f;
uint cell = direction > 0 ? SeamCellWest : SeamCellEast;
var pos = new Vector3(startX, -40f, -6f);
float? flipX = null;
for (int tick = 0; tick < 26; tick++)
{
var target = pos + new Vector3(direction * step, 0f, 0f);
var r = engine.ResolveWithTransition(
currentPos: pos,
targetPos: target,
cellId: cell,
sphereRadius: 0.48f,
sphereHeight: 1.835f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide);
bool flipped = r.CellId != cell;
_out.WriteLine($"tick={tick,2} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) " +
$"cell=0x{r.CellId:X8} ok={r.Ok}{(flipped ? " <<< FLIP" : "")}");
if (flipped && flipX is null)
flipX = r.Position.X;
cell = r.CellId;
pos = r.Position;
if (direction > 0 && pos.X > 86.4f) break;
if (direction < 0 && pos.X < 83.6f) break;
}
Assert.NotNull(flipX);
float lag = direction > 0 ? flipX!.Value - 85.00f : 85.00f - flipX!.Value;
_out.WriteLine($"flip at x={flipX:F3} → lag past the plane = {lag:F3} m " +
$"(one-tick quantization bound = {step:F3} m)");
// Diagnostic, not a pin: the finding is the printed lag. A lag beyond
// one tick step is the divergence under investigation.
}
}