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:
parent
544d4d3eef
commit
b8e9e204ad
5 changed files with 1176 additions and 19 deletions
|
|
@ -0,0 +1,280 @@
|
|||
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>
|
||||
/// #176 (purple flashing at corridor seams, camera-angle dependent) + #177
|
||||
/// (stairs pop in/out) — headless portal-flood replay in the Facility Hub
|
||||
/// landblock 0x8A02. The unified hypothesis after the dat + draw-path reads:
|
||||
/// both artifacts are FLOOD ADMISSION instability (a cell dropping out of
|
||||
/// PortalVisibilityBuilder's admitted set paints the fog-purple clear color
|
||||
/// where its geometry was; stair cells failing admission = the pop).
|
||||
///
|
||||
/// Production-matched inputs: Build(root, eye, lookup, viewProj,
|
||||
/// buildingMembership: null, drawLiftZ: ShellDrawLiftZ) — the drawLiftZ
|
||||
/// mirrors RetailPViewRenderer.DrawInside.
|
||||
///
|
||||
/// Scenarios:
|
||||
/// A. #177 approach — stand in corridor 0x0178, look +X at the stair ramp
|
||||
/// (0x0182) and the lower cell (0x0183): are they admitted?
|
||||
/// B. #177 descent — eye path down the ramp crossing into 0x0183: does
|
||||
/// 0x0182 (the ramp geometry's owner) drop near the transit?
|
||||
/// C. #176 gaze sweep — eye parked in 0x016E near the 0x017A seam, yaw
|
||||
/// sweep at several pitches: any cell admitted at angle k, gone at k+1,
|
||||
/// back at k+2 (the bistability signature)?
|
||||
/// D. #176 walk — eye tracks down the corridor across two seams, gaze
|
||||
/// locked +X: per-step admitted-set diffs (drop-for-one-step churn).
|
||||
/// </summary>
|
||||
public class Issue176177FacilityHubFloodReplayTests
|
||||
{
|
||||
private const uint FacilityHub = 0x8A020000u;
|
||||
|
||||
private readonly ITestOutputHelper _out;
|
||||
public Issue176177FacilityHubFloodReplayTests(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(1.2f, 1280f / 720f, 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 ScenarioA_StairApproach_AdmissionsFromCorridor()
|
||||
{
|
||||
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(FacilityHub | 0x0178u), "0x0178 not loaded");
|
||||
|
||||
// 0x0178 spans x<=95 at z −6..−3 (floor −6); the ramp cell 0x0182 runs
|
||||
// x 95→98.33 descending; 0x0183 continues at z −9 beyond x=98.33.
|
||||
// Eye at standing height (~1.7 m above the −6 floor), approaching the
|
||||
// stair portal at x=95, gazing +X with a slight downward pitch (the
|
||||
// natural look at a descending stair).
|
||||
foreach (float eyeX in new[] { 88f, 90f, 92f, 94f, 94.9f })
|
||||
{
|
||||
var eye = new Vector3(eyeX, -40f, -4.3f);
|
||||
foreach (var (gaze, label) in new (Vector3, string)[]
|
||||
{
|
||||
(new Vector3(1f, 0f, 0f), "level"),
|
||||
(Vector3.Normalize(new Vector3(1f, 0f, -0.35f)), "pitch-19"),
|
||||
(Vector3.Normalize(new Vector3(1f, 0f, -0.7f)), "pitch-35"),
|
||||
})
|
||||
{
|
||||
var visible = Flood(cells, FacilityHub | 0x0178u, eye, gaze);
|
||||
bool ramp = visible.Contains(FacilityHub | 0x0182u);
|
||||
bool lower = visible.Contains(FacilityHub | 0x0183u);
|
||||
_out.WriteLine($"eyeX={eyeX,5:F1} gaze={label,-8} flood={visible.Count,2} " +
|
||||
$"ramp0182={(ramp ? "Y" : "MISSING")} lower0183={(lower ? "Y" : "MISSING")} " +
|
||||
$"[{CellSetString(visible)}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScenarioB_StairDescent_RampCellRetention()
|
||||
{
|
||||
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);
|
||||
|
||||
// Descend the ramp: floor z goes −6 (x=95) → −9 (x=98.33), then flat.
|
||||
// Eye rides ~1.7 m above the local floor. Root = the cell containing
|
||||
// the eye by x-range (0x0178 x<95, ramp 0x0182 95..98.33, 0x0183 after).
|
||||
// Gaze: forward and slightly down (running down stairs).
|
||||
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.4f));
|
||||
List<uint>? prev = null;
|
||||
for (float x = 94.0f; x <= 100.5f; x += 0.1f)
|
||||
{
|
||||
float floorZ = x < 95f ? -6f
|
||||
: x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f
|
||||
: -9f;
|
||||
var eye = new Vector3(x, -40f, floorZ + 1.7f);
|
||||
uint rootId = x < 95f ? FacilityHub | 0x0178u
|
||||
: x < 98.333f ? FacilityHub | 0x0182u
|
||||
: FacilityHub | 0x0183u;
|
||||
|
||||
var visible = Flood(cells, rootId, eye, gazeDir);
|
||||
bool ramp = visible.Contains(FacilityHub | 0x0182u);
|
||||
bool upper = visible.Contains(FacilityHub | 0x0178u);
|
||||
|
||||
string diff = "";
|
||||
if (prev is not null)
|
||||
{
|
||||
var removed = prev.Except(visible).ToList();
|
||||
var added = visible.Except(prev).ToList();
|
||||
if (removed.Count > 0) diff += $" REMOVED=[{CellSetString(removed)}]";
|
||||
if (added.Count > 0) diff += $" added=[{CellSetString(added)}]";
|
||||
}
|
||||
_out.WriteLine($"x={x,6:F1} root={rootId & 0xFFFFu:X4} eyeZ={eye.Z,6:F2} flood={visible.Count,2} " +
|
||||
$"ramp0182={(ramp ? "Y" : "MISSING")} up0178={(upper ? "Y" : "-")}{diff}");
|
||||
prev = visible;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScenarioC_CorridorSeamGazeSweep_Bistability()
|
||||
{
|
||||
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);
|
||||
|
||||
// Parked eye in 0x016E near the 0x017A seam (x=85), standing height.
|
||||
var eye = new Vector3(83.5f, -40f, -4.3f);
|
||||
uint rootId = FacilityHub | 0x016Eu;
|
||||
|
||||
int churnEvents = 0;
|
||||
foreach (float pitchZ in new[] { 0f, -0.35f, -0.75f, -1.2f })
|
||||
{
|
||||
List<uint>? prevSet = null;
|
||||
float prevYaw = 0f;
|
||||
var perYawSets = new List<(float yaw, List<uint> set)>();
|
||||
for (float yawDeg = -180f; yawDeg <= 180f; yawDeg += 2f)
|
||||
{
|
||||
float rad = yawDeg * MathF.PI / 180f;
|
||||
var gaze = Vector3.Normalize(new Vector3(MathF.Cos(rad), MathF.Sin(rad), pitchZ));
|
||||
var visible = Flood(cells, rootId, eye, gaze);
|
||||
perYawSets.Add((yawDeg, visible));
|
||||
|
||||
if (prevSet is not null)
|
||||
{
|
||||
var removed = prevSet.Except(visible).ToList();
|
||||
var added = visible.Except(prevSet).ToList();
|
||||
if (removed.Count > 0 || added.Count > 0)
|
||||
{
|
||||
_out.WriteLine($"pitch={pitchZ,5:F2} yaw {prevYaw,6:F0}->{yawDeg,6:F0}: " +
|
||||
$"flood {prevSet.Count}->{visible.Count}" +
|
||||
(removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") +
|
||||
(added.Count > 0 ? $" added=[{CellSetString(added)}]" : ""));
|
||||
}
|
||||
}
|
||||
prevSet = visible;
|
||||
prevYaw = yawDeg;
|
||||
}
|
||||
|
||||
// Bistability: a cell present at yaw k, absent at k+1, present at k+2.
|
||||
for (int i = 2; i < perYawSets.Count; i++)
|
||||
{
|
||||
var flicker = perYawSets[i - 2].set
|
||||
.Intersect(perYawSets[i].set)
|
||||
.Except(perYawSets[i - 1].set)
|
||||
.ToList();
|
||||
if (flicker.Count > 0)
|
||||
{
|
||||
churnEvents++;
|
||||
_out.WriteLine($">>> BISTABLE pitch={pitchZ:F2} yaw={perYawSets[i - 1].yaw:F0}: " +
|
||||
$"cells [{CellSetString(flicker)}] dropped for ONE 2-degree step");
|
||||
}
|
||||
}
|
||||
}
|
||||
_out.WriteLine($"bistable one-step drop events: {churnEvents}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// THE production lag-window scenario (from launch-137-gate2.log
|
||||
/// [cell-transit] lines): membership transits fire 0.1–0.6 m PAST the
|
||||
/// portal plane in the travel direction (016E→017A at x=85.33–85.47 vs
|
||||
/// the plane at x=85.00; 0182→0183 at 98.56–98.64 vs 98.33). The render
|
||||
/// root (viewer cell, same membership machinery) therefore holds the OLD
|
||||
/// cell while the camera eye is already beyond the boundary portal's
|
||||
/// plane. This scenario reproduces exactly that window: root=old cell,
|
||||
/// eye stepped across and past the plane, gaze forward. If the forward
|
||||
/// chain (the next corridor cells) drops inside the window, that is #176
|
||||
/// (purple = fog clear color where the forward cells' geometry was) and
|
||||
/// #177(a)/(c) at the stair transit.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0x016Eu, 0x017Au, 85.00f, -4.3f)] // corridor seam, plane x=85
|
||||
[InlineData(0x0182u, 0x0183u, 98.333f, -7.3f)] // stair-bottom transit, plane x=98.33
|
||||
public void ScenarioE_RootLagWindow_ForwardChainRetention(
|
||||
uint rootLow, uint forwardLow, float planeX, float eyeZ)
|
||||
{
|
||||
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 rootId = FacilityHub | rootLow;
|
||||
uint forwardId = FacilityHub | forwardLow;
|
||||
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f));
|
||||
|
||||
_out.WriteLine($"root=0x{rootLow:X4} forward=0x{forwardLow:X4} plane x={planeX:F2} " +
|
||||
"(eye sweeps across; root HELD at the old cell = the production lag window)");
|
||||
foreach (float dx in new[] { -0.30f, -0.10f, -0.02f, 0.00f, 0.02f, 0.05f, 0.10f, 0.20f, 0.30f, 0.45f, 0.60f })
|
||||
{
|
||||
var eye = new Vector3(planeX + dx, -40f, eyeZ);
|
||||
var visible = Flood(cells, rootId, eye, gazeDir);
|
||||
bool fwd = visible.Contains(forwardId);
|
||||
_out.WriteLine($" eyeX=plane{(dx >= 0 ? "+" : "")}{dx:F2} flood={visible.Count,2} " +
|
||||
$"forward={(fwd ? "Y" : ">>> DROPPED <<<")} [{CellSetString(visible)}]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScenarioD_CorridorWalk_PerStepChurn()
|
||||
{
|
||||
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);
|
||||
|
||||
// Walk 0x0165 → 0x016E → 0x017A along y=−40 (seams at x=75 and x=85),
|
||||
// gaze locked +X, slight downward pitch (the running view). Root flips
|
||||
// by x-range at the seams (the camera transits the same portals).
|
||||
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f));
|
||||
List<uint>? prev = null;
|
||||
int churn = 0;
|
||||
for (float x = 71f; x <= 89f; x += 0.05f)
|
||||
{
|
||||
uint rootId = x < 75f ? FacilityHub | 0x0165u
|
||||
: x < 85f ? FacilityHub | 0x016Eu
|
||||
: FacilityHub | 0x017Au;
|
||||
if (!cells.ContainsKey(rootId)) continue;
|
||||
var eye = new Vector3(x, -40f, -4.3f);
|
||||
var visible = Flood(cells, rootId, eye, gazeDir);
|
||||
|
||||
if (prev is not null)
|
||||
{
|
||||
var removed = prev.Except(visible).ToList();
|
||||
var added = visible.Except(prev).ToList();
|
||||
if (removed.Count > 0 || added.Count > 0)
|
||||
{
|
||||
churn++;
|
||||
_out.WriteLine($"x={x,6:F2} root={rootId & 0xFFFFu:X4} flood={visible.Count,2}" +
|
||||
(removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") +
|
||||
(added.Count > 0 ? $" added=[{CellSetString(added)}]" : ""));
|
||||
}
|
||||
}
|
||||
prev = visible;
|
||||
}
|
||||
_out.WriteLine($"admitted-set change events over the walk: {churn}");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue