diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index f9c24691..04fa708c 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -299,7 +299,100 @@ investigation (`docs/research/2026-07-06-176-177-render-pair-investigation.md`).
## #177 β Dungeon stairs pop in/out across levels (invisible until entering the room; last step vanishes running down)
-**β οΈ UPDATE 2026-07-06 (visual gate) β this is NOT lighting.** The A7 visible-cell
+**π
ΏοΈ PARKED 2026-07-07** (user decision, after retail cdb disproved the portal-flood theory β
+see session-3b below). NOT a blocker (cosmetic indoor pop). Ruled out with evidence: lighting,
+membership, camera coherence, the collision sweep, the `0178/0182/0183` handoff cells, edge-on
+eye-in-opening (fix#1 shippedβgate-failedβREVERTED), and the portal flood itself (retail's flood
+collapses identically). **Freshest un-chased lead:** the steps are STATIC objects (GfxObj
+`0x010000DE` Γ6/cell) drawn via the separate viewcone cull, NOT cell shell β probe acdream's
+step-static draw vs retail's. Characterization pins + the reusable retail-cdb capture toolchain
+(`tools/cdb/pview-spiral2.cdb`, eye + `cell_draw_list` dump, clean top-level `qd` detach) are
+committed. Resume from the session-3b block.
+
+**β οΈ UPDATE 2026-07-07 (session 3b) β cdb-traced RETAIL; the PORTAL-FLOOD theory is
+DISPROVEN by retail's own ground truth.** Attached cdb to live retail (PDB MATCH),
+broke on `PView::DrawCells` (0x005a4840), dumped `cell_draw_num` + `cell_draw_list` cell
+ids (`.data[i]->m_DID.id`) + the eye (`Render::FrameCurrent->viewer.viewpoint`) while the
+user descended the spiral (`retail-spiral2.log`, 767 samples). **Retail's flood is dynamic
+and IDENTICAL in character to ours** β from the spiral cells it swings `num` 3β27 with gaze
+and COLLAPSES to 3 cells at many poses (e.g. cam=015d eye(60.05,-28.51,-3.66) β just
+{015d 015e 015f}; cam=014b β {014b 014c 014a}). Our flood does the same (3β43, headless
+`FloodDepthFrom015E_VsRetail26` = max 43 vs retail 26). So retail does NOT keep the spiral
+where we drop it β the flood is EXONERATED as the cause. **The vanish must be DOWNSTREAM of
+the flood** (unexplored): the steps in these spiral cells are STATIC objects (GfxObj
+`0x010000DE` Γ6/cell), drawn via the separate viewcone cull (`ViewconeCuller.SphereVisibleInCell`
+/ `DrawCellObjectLists`), NOT the cell shell β acdream may cull those step statics where
+retail doesn't. OR a per-pose clip difference at the exact descending gaze (would need
+retail's gaze β not yet captured β to confirm, but the matching num RANGES argue against it).
+cdb watchout CONFIRMED THE HARD WAY: `qd` in a CONDITIONAL bp action does NOT fire (stuck
+cdb β had to kill it β took retail down). Reliable detach = bp action falls through to BREAK
+after N (no gc), then top-level `qd` (`tools/cdb/pview-spiral2.cdb`). NEXT: probe acdream's
+step-static (0x010000DE) draw/viewcone in the spiral vs the cell shell.
+
+**β οΈ UPDATE 2026-07-07 (session 3) β fix#1 (edge-on eye-in-opening rescue) FAILED the
+visual gate + REVERTED; [flood-collapse framing superseded by 3b β retail's flood collapses
+the same way, so the flood is not the differentiator]. The mechanism is a GRAZING/SLIVER
+FLOOD COLLAPSE in the tight SPIRAL staircase, NOT the edge-on-in-plane case.** Live [stair-clip]+[flap] capture
+(`launch-177-stairclip.log`, PROBE_FLAP): the user's staircase is the `0x8A02` SPIRAL
+`01C0β01C1ββ¦β01C8β0210ββ¦` (cells stacked z 0..6, joined by floor/ceiling portals + turning
+via alternating Β±Y wall portals; the recurring `0x010000DE`Γ6 statics are the steps). At the
+vanish, root=`01C8`, vis collapses **12β3**: the DOWN floor-portal `β0210` goes OFF-SCREEN
+(`clip=0`, D=3.75 β NOT edge-on) and the wall portals `β01C4/01C9` project to SLIVERS
+(`clip=3`), so the flood barely spreads and 9 cells (the spiral) drop. Turning slightly
+re-admits (portals swing on-screen); zoom-out gives a top-down shaft view (aligned floor
+portals) β floods down β visible. **The camera is NOT collision-jammed** β `[flap-sweep]`
+`pulledIn=0.00` across all 103k frames, full boom, `collNormValid=False` β so it is purely
+the flood/clip, from a NATURAL chase pose. This is the #119/#181 grazing/sliver-clip class.
+**The retail contradiction (hit 3Γ):** retail draws only the flood (`PView::DrawCells`
+`cell_draw_list`) and `polyClipFinish` also rejects <3-vert slivers β so retail's flood
+*should* collapse the same way, yet the user's retail shows the spiral. Something about
+retail's viewpoint or flood in a spiral differs in a way NOT derivable from static analysis.
+**NEXT: cdb-trace retail descending this spiral** (capture `PView::ConstructView` β
+`cell_draw_list` contents + `Render::FrameCurrent->viewer.viewpoint`) to see how retail keeps
+the spiral flooded β the CLAUDE.md tool for "guessing failed twice." Characterization pins +
+the [stair-clip] probe (reverted from the shipped path) live in
+`Issue177StairDescentCameraFloodTests`. DO-NOT-RETRY: the edge-on eye-in-opening rescue
+(fix#1) β the vanish D is never near 0; a camera-press fix β the sweep is never pulled in.
+
+**β οΈ UPDATE 2026-07-06 (session 2) β [superseded by session 3; the edge-on sub-case below
+is real but is NOT the production vanish]. ROOT CAUSE RE-DIAGNOSED with production-faithful
+evidence; the two prior attributions below (lighting; then "flood-admission miss at
+0178/0182/0183") are BOTH SUPERSEDED.** Findings, all evidence-backed (headless real-camera
+harness `Issue177StairDescentCameraFloodTests` + production `[flap]/[flap-cam]/[vis]/[cell-transit]`
+capture `launch-177-flapcell2.log`):
+- **NOT membership, NOT camera coherence, NOT the collision sweep.** Across the whole
+ capture: `eyeInRoot=Y` on every frame (0 anomalies), the camera sweep never failed
+ (0 failures). The render root always contains the eye. My headless harness driving the
+ REAL chase camera + REAL sweep down the stairs = `collapses=0, incoherentFrames=0`.
+- **The real staircase the user tests is `0x8A02015E/015F/01C1`** (stacked vertically,
+ joined by **floor/ceiling portals** N=(0,0,Β±1), 6β7 statics each) β NOT `0178/0182/0183`
+ (those have no statics, no floor portals). The handoff mis-identified the staircase.
+- **The steps are cell SHELL geometry** (drawn whole/unconditionally when the cell is
+ admitted), so the vanish β the cell drops from the flood.
+- **MECHANISM: you can flood DOWN a staircase but not UP it.** `[vis]` sets: root `01C1`
+ (top) β {015F,015E}; root `015F` drops `01C1`; root `015E` drops both `015F` AND `01C1`.
+ The up-cell dies at the **CLIP, never the side test** (root `015F`β`01C1`: CULL=0,
+ edge-on clipβ€0 = 1508/1782). The chase-camera eye on a staircase sits at/near the cell
+ boundary = the floor/ceiling portal PLANE (local eye-z β 6.0 = the ceiling, `Dβ0`),
+ so the vertical portal projects EDGE-ON (collinear, <3 verts) β `ClipPortalAgainstView`
+ rejects it β the stacked cell above never floods in β the whole staircase above the
+ camera vanishes. Descending puts the camera below the stairs (vanish); looking back
+ from the lower cell (vanish); zoom-out lifts the camera to a higher root that floods
+ DOWN (floor portals admit) β visible. It's the #119/#181 edge-on-clip class, on VERTICAL
+ (floor/ceiling) portals viewed from a boundary-height eye.
+- **Retail complication (why the fix is NOT a naive clip relax):** `PView::DrawCells`
+ (0x005a4840) draws `cell_draw_list` (the portal flood), and `polyClipFinish` ALSO
+ rejects <3 survivors (docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md
+ lines 39,59). So retail's flood would drop an edge-on portal too β meaning retail's
+ eye must NOT sit edge-on with floor/ceiling portals (leading hypothesis: retail's flood
+ viewpoint is mid-cell / the player-eye at ~1.7 m above the floor, never in a floor
+ portal's plane; our chase-cam eye rides up to the ceiling portal). NEXT: confirm retail's
+ stair-camera eye height (cdb) OR the flood-seed viewpoint, then fix retail-faithfully
+ (camera-side keep-eye-off-portal-planes vs a narrow boundary-eye flood rule). DO NOT
+ relax `ClipToRegion`'s <3 gate (diverges from retail + risks #119/#181).
+
+**β οΈ UPDATE 2026-07-06 (visual gate) β this is NOT lighting.** [SUPERSEDED by session-2
+block above.] The A7 visible-cell
light-scoping fix shipped + was probe-validated, but the user's gate showed the stairs
STILL not visible looking back from the corridor (zoom-out changes the last-step case).
Eye-position/flood behavior β a portal-VISIBILITY miss at the stair cells
diff --git a/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs b/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs
new file mode 100644
index 00000000..2652301b
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Issue177StairDescentCameraFloodTests.cs
@@ -0,0 +1,560 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using AcDream.App.Rendering;
+using AcDream.Core.Physics;
+using AcDream.Core.Rendering;
+using DatReaderWriter;
+using DatReaderWriter.Options;
+using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
+using DatEnvironment = DatReaderWriter.DBObjs.Environment;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace AcDream.App.Tests.Rendering;
+
+///
+/// #177 decisive full-chain replay: drive the REAL
+/// + REAL down the Facility Hub
+/// staircase against the REAL BSP, then feed the swept (ViewerCellId, eye,
+/// View*Projection) into β exactly
+/// the production coupling (GameWindow ~9055-9065 + RetailPViewRenderer.DrawInside).
+///
+/// The earlier Issue176177 replays fed SYNTHETIC (root, eye) pairs: Scenario A
+/// admitted the stairs fine from a coherent corridor root, Scenario E collapsed
+/// the flood 12->1 but only because it HELD the root at the old cell while the
+/// eye crossed a portal plane. This test removes the assumption: the root IS the
+/// swept camera cell, the eye IS the swept camera eye, both from the same sweep.
+/// If the stairs still collapse, #177 is a production root/eye coupling defect
+/// and the printed per-portal side-test D names the failing gate. If they stay
+/// admitted, the flood is exonerated and the artifact lives downstream.
+///
+public class Issue177StairDescentCameraFloodTests
+{
+ private const uint FacilityHub = 0x8A020000u;
+
+ private readonly ITestOutputHelper _out;
+ public Issue177StairDescentCameraFloodTests(ITestOutputHelper output) => _out = output;
+
+ private static string? ResolveDatDir()
+ {
+ var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
+ if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) return fromEnv;
+ var def = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ "Documents", "Asheron's Call");
+ return Directory.Exists(def) ? def : null;
+ }
+
+ private static PhysicsEngine BuildHubEngine(DatCollection dats)
+ {
+ var cache = new PhysicsDataCache();
+ var engine = new PhysicsEngine { DataCache = cache };
+ for (uint low = 0x0100u; low <= 0x01FFu; low++)
+ {
+ uint id = FacilityHub | low;
+ var datCell = dats.Get(id);
+ if (datCell is null) continue;
+ var environment = dats.Get(0x0D000000u | datCell.EnvironmentId);
+ if (environment is null) continue;
+ if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct) || cellStruct is null)
+ continue;
+ var world = Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation) *
+ Matrix4x4.CreateTranslation(datCell.Position.Origin);
+ cache.CacheCellStruct(id, datCell, cellStruct, world);
+ }
+ var heights = new byte[81];
+ var heightTable = new float[256];
+ for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
+ engine.AddLandblock(FacilityHub, new TerrainSurface(heights, heightTable),
+ Array.Empty(), Array.Empty(), 0f, 0f);
+ return engine;
+ }
+
+ // Analytic ramp floor from Scenario B: flat -6 (x<95), descends to -9 across
+ // x 95..98.333, then flat -9.
+ private static float FloorZ(float x)
+ => x < 95f ? -6f
+ : x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f
+ : -9f;
+
+ // Replicated side test (PortalVisibilityBuilder.CameraOnInteriorSide is private).
+ private const float PortalSideEpsilon = 0.01f;
+ private static bool EyeInteriorSide(LoadedCell cell, int i, Vector3 eye)
+ {
+ if (i >= cell.ClipPlanes.Count) return true;
+ var pl = cell.ClipPlanes[i];
+ if (pl.Normal.LengthSquared() < 1e-8f) return true;
+ var local = Vector3.Transform(eye, cell.InverseWorldTransform);
+ float dot = Vector3.Dot(pl.Normal, local) + pl.D;
+ return pl.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon;
+ }
+ private static float SideD(LoadedCell cell, int i, Vector3 eye)
+ {
+ if (i >= cell.ClipPlanes.Count) return float.NaN;
+ var pl = cell.ClipPlanes[i];
+ if (pl.Normal.LengthSquared() < 1e-8f) return float.NaN;
+ var local = Vector3.Transform(eye, cell.InverseWorldTransform);
+ return Vector3.Dot(pl.Normal, local) + pl.D;
+ }
+
+ private static string CellSet(IEnumerable ids)
+ => string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
+
+ [Fact]
+ public void StairCellComposition_ShellVsStatics()
+ {
+ var datDir = ResolveDatDir();
+ if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+
+ foreach (uint low in new uint[] { 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 })
+ {
+ uint id = FacilityHub | low;
+ var envCell = dats.Get(id);
+ if (envCell is null) { _out.WriteLine($"0x{low:X4}: (no dat cell)"); continue; }
+ var environment = dats.Get(0x0D000000u | envCell.EnvironmentId);
+ environment!.Cells.TryGetValue(envCell.CellStructure, out var cs);
+
+ int polys = cs?.Polygons.Count ?? -1;
+ int verts = cs?.VertexArray.Vertices.Count ?? -1;
+ var mn = new Vector3(float.MaxValue); var mx = new Vector3(float.MinValue);
+ if (cs is not null)
+ foreach (var kv in cs.VertexArray.Vertices)
+ { var o = kv.Value.Origin; var v = new Vector3(o.X, o.Y, o.Z); mn = Vector3.Min(mn, v); mx = Vector3.Max(mx, v); }
+
+ int statics = envCell.StaticObjects?.Count ?? 0;
+ bool seenOut = (envCell.Flags & DatReaderWriter.Enums.EnvCellFlags.SeenOutside) != 0;
+ _out.WriteLine(FormattableString.Invariant(
+ $"0x{low:X4} env=0x{envCell.EnvironmentId:X4} struct={envCell.CellStructure} shellPolys={polys} shellVerts={verts} bounds=[{mn.X:F1},{mn.Y:F1},{mn.Z:F1}]..[{mx.X:F1},{mx.Y:F1},{mx.Z:F1}] portals={envCell.CellPortals.Count} statics={statics} seenOut={seenOut}"));
+ if (envCell.StaticObjects is not null)
+ foreach (var stab in envCell.StaticObjects)
+ {
+ var o = stab.Frame.Origin;
+ _out.WriteLine(FormattableString.Invariant(
+ $" static Id=0x{stab.Id:X8} kind={(stab.Id >> 24 == 0x02 ? "Setup" : stab.Id >> 24 == 0x01 ? "GfxObj" : "?")} pos=({o.X:F2},{o.Y:F2},{o.Z:F2})"));
+ }
+
+ // Portal normals (local space) β horizontal (|Nz|~1) = FLOOR/CEILING portal
+ // (the edge-on-clip suspect); vertical (|Nz|~0) = wall/doorway portal.
+ var lc = renderLookup(id);
+ if (lc is not null)
+ for (int i = 0; i < lc.Portals.Count; i++)
+ {
+ var n = i < lc.ClipPlanes.Count ? lc.ClipPlanes[i].Normal : Vector3.Zero;
+ string kind = MathF.Abs(n.Z) > 0.7f ? "FLOOR/CEIL" : MathF.Abs(n.Z) < 0.3f ? "wall" : "slope";
+ _out.WriteLine(FormattableString.Invariant(
+ $" portal[{i}]->0x{lc.Portals[i].OtherCellId:X4} N=({n.X:F2},{n.Y:F2},{n.Z:F2}) {kind} inside={lc.ClipPlanes[i].InsideSide}"));
+ }
+ }
+
+ LoadedCell? renderLookup(uint id)
+ {
+ try { return CornerFloodReplayTests.LoadCell(dats, id); }
+ catch (InvalidOperationException) { return null; }
+ }
+ }
+
+ ///
+ /// #177 re-diagnosis (fix#1 failed the visual gate): the vanish flips on a SLIGHT turn +
+ /// depends on zoom, so it is a gaze/eye knife-edge, not the eye-squarely-in-opening case
+ /// fix#1 handled. Drive the REAL camera at a fixed player spot on the 015F stairs, sweep
+ /// yaw FINELY at two zooms, and print the viewer cell + stair-cell admission each step to
+ /// locate the knife-edge and which cell flips.
+ ///
+ [Fact]
+ public void RealStaircase_FineYawZoomSweep_FindKnifeEdge()
+ {
+ var datDir = ResolveDatDir();
+ if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+ var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
+ var engine = BuildHubEngine(dats);
+ Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
+
+ bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
+ try
+ {
+ CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
+ CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
+
+ uint[] stair = { 0x015Eu, 0x015Fu, 0x01C1u, 0x01C0u, 0x020Fu };
+ // Player standing mid-015F stairs, eye near the ceiling boundary (per the sweep char).
+ foreach (var playerPos in new[] { new Vector3(60.5f, -46f, -2.5f), new Vector3(60.5f, -47f, -3.0f) })
+ foreach (float dist in new[] { 2.61f, 12.0f })
+ {
+ _out.WriteLine(FormattableString.Invariant(
+ $"=== player=({playerPos.X:F1},{playerPos.Y:F1},{playerPos.Z:F1}) zoom={dist} ==="));
+ var cam = new RetailChaseCamera
+ {
+ CollisionProbe = new PhysicsCameraCollisionProbe(engine),
+ FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist,
+ };
+ uint prevViewer = 0; string prevAdmit = "";
+ for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 6f)
+ {
+ float yaw = yawDeg * MathF.PI / 180f;
+ var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f));
+ for (int i = 0; i < 160; i++)
+ cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
+ var eye = cam.Position; uint viewer = cam.ViewerCellId;
+ string admit = "?";
+ if (viewer != 0u && renderCells.TryGetValue(viewer, out var rc))
+ {
+ var f = PortalVisibilityBuilder.Build(rc, eye, lookup, cam.View * cam.Projection, null, PortalVisibilityBuilder.ShellDrawLiftZ);
+ var s = f.OrderedVisibleCells;
+ admit = string.Concat(stair.Select(low => (s.Contains(FacilityHub | low) ? "1" : "0")));
+ }
+ // Print only when viewer or the admit pattern CHANGES (knife-edges).
+ if (viewer != prevViewer || admit != prevAdmit)
+ {
+ _out.WriteLine(FormattableString.Invariant(
+ $" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} stair[5E,5F,C1,C0,0F]={admit}"));
+ prevViewer = viewer; prevAdmit = admit;
+ }
+ }
+ }
+ }
+ finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
+ }
+
+ ///
+ /// #177 retail-vs-us flood DEPTH: retail's cdb capture shows PView cell_draw_num reaching 26
+ /// cells from camera cell 015E (avg 14.3) looking down the spiral shaft. Sweep OUR real camera
+ /// from 015E over many gaze/zoom poses and report the max/avg OrderedVisibleCells β if ours
+ /// caps well below retail's 26, our flood truncates the spiral (the vanish).
+ ///
+ [Fact]
+ public void FloodDepthFrom015E_VsRetail26()
+ {
+ var datDir = ResolveDatDir();
+ if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+ var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
+ var engine = BuildHubEngine(dats);
+ Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
+
+ bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
+ try
+ {
+ CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
+ CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
+ int maxCells = 0; uint bestViewer = 0; double sum = 0; int n = 0; string bestSet = "";
+ foreach (var playerPos in new[] { new Vector3(61f, -42f, -4f), new Vector3(60f, -41f, -5f), new Vector3(62f, -43f, -4f) })
+ foreach (float dist in new[] { 2.61f, 4f, 8f })
+ foreach (float pitch in new[] { 0.291f, 0.7f, -0.2f })
+ {
+ var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist, Pitch = pitch };
+ for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 10f)
+ {
+ float yaw = yawDeg * MathF.PI / 180f;
+ var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Eu, playerPos + new Vector3(0, 0, 0.1f));
+ for (int i = 0; i < 140; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
+ uint viewer = cam.ViewerCellId;
+ if (viewer == 0u || !renderCells.TryGetValue(viewer, out var rc)) continue;
+ var f = PortalVisibilityBuilder.Build(rc, cam.Position, lookup, cam.View * cam.Projection, null, PortalVisibilityBuilder.ShellDrawLiftZ);
+ int c = f.OrderedVisibleCells.Count; sum += c; n++;
+ if (c > maxCells) { maxCells = c; bestViewer = viewer; bestSet = CellSet(f.OrderedVisibleCells.OrderBy(v => v)); }
+ }
+ }
+ _out.WriteLine(FormattableString.Invariant(
+ $"OUR flood from 015E-area: maxCells={maxCells} (retail max=26) avg={sum / Math.Max(1, n):F1} (retail avg=14.3) bestViewer=0x{bestViewer & 0xFFFF:X4}"));
+ _out.WriteLine($" best set: [{bestSet}]");
+ }
+ finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
+ }
+
+ private static Matrix4x4 ViewProj(Vector3 eye, Vector3 dir)
+ => Matrix4x4.CreateLookAt(eye, eye + Vector3.Normalize(dir), Vector3.UnitZ)
+ * Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f);
+
+ // Signed distance of a WORLD point to a render cell's portal[i] plane (local-space plane).
+ private static float PortalPlaneDistance(LoadedCell cell, int i, Vector3 worldPt)
+ {
+ var pl = cell.ClipPlanes[i];
+ var local = Vector3.Transform(worldPt, cell.InverseWorldTransform);
+ return Vector3.Dot(pl.Normal, local) + pl.D;
+ }
+
+ ///
+ /// #177 THE decisive experiment. Reproduce the captured failing frame β camera in
+ /// cell 0x015F with the eye sitting IN the ceiling-portal plane (production [flap]:
+ /// root=015F eye=(60.47,-46.57,-0.00), p->01C1 clip=0) β and confirm the upper
+ /// stair cell 01C1 drops from the flood. Then move the SAME eye 0.4 m off that
+ /// plane (into the cell interior) and confirm 01C1 is re-admitted. If the contrast
+ /// holds, #177's fix is a viewpoint-off-the-portal-plane rule, and the load-bearing
+ /// clip (#119/#181) is correct as-is.
+ ///
+ [Fact]
+ public void EdgeOnCeilingPortal_DropsUpperCell_OffPlaneReadmits()
+ {
+ var datDir = 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);
+ Func lookup = id => cells.TryGetValue(id, out var c) ? c : null;
+
+ var root = cells[FacilityHub | 0x015Fu];
+ uint upperId = FacilityHub | 0x01C1u;
+
+ // Find 015F's portal to 01C1 (the ceiling/up portal) + report its normal.
+ int upIdx = -1;
+ for (int i = 0; i < root.Portals.Count; i++)
+ if (root.Portals[i].OtherCellId == 0x01C1u) { upIdx = i; break; }
+ Assert.True(upIdx >= 0, "015F has no portal to 01C1");
+ var upN = root.ClipPlanes[upIdx].Normal;
+ _out.WriteLine(FormattableString.Invariant(
+ $"015F portal[{upIdx}]->01C1 localN=({upN.X:F2},{upN.Y:F2},{upN.Z:F2})"));
+
+ // Look UP the stairs so the ceiling portal + 01C1 land on screen.
+ var gaze = new Vector3(0f, -0.3f, 1f);
+
+ // Captured FAILING eye β in the ceiling-portal plane (world z ~0).
+ var eyeEdgeOn = new Vector3(60.47f, -46.57f, -0.00f);
+ // Same eye, pulled 0.4 m DOWN (off the ceiling portal, into 015F's interior).
+ var eyeOffPlane = eyeEdgeOn + new Vector3(0f, 0f, -0.40f);
+
+ foreach (var (label, eye) in new[] { ("edge-on(captured)", eyeEdgeOn), ("off-plane(-0.4z)", eyeOffPlane) })
+ {
+ float d = PortalPlaneDistance(root, upIdx, eye);
+ var frame = PortalVisibilityBuilder.Build(
+ root, eye, lookup, ViewProj(eye, gaze),
+ buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
+ var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
+ bool up = vis.Contains(upperId);
+ string admit = up ? "ADMITTED" : "DROPPED";
+ _out.WriteLine(FormattableString.Invariant(
+ $"{label,-18} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) D->01C1={d,6:F3} 01C1={admit} flood={vis.Count} [{CellSet(vis)}]"));
+ }
+
+ // #177 CHARACTERIZATION (fix#1 reverted β this edge-on-in-plane case was NOT the
+ // production mechanism; the real vanish is a grazing/sliver flood collapse in the
+ // spiral, root 0x01C8, portals off-screen/sliver, camera NOT collision-jammed).
+ // Kept as a mechanism pin: an eye exactly in the ceiling-portal plane drops the upper
+ // cell (edge-on β <3 clip), an eye 0.4 m off admits it via the normal clip.
+ var fEdge = PortalVisibilityBuilder.Build(root, eyeEdgeOn, lookup, ViewProj(eyeEdgeOn, gaze), null, PortalVisibilityBuilder.ShellDrawLiftZ);
+ var fOff = PortalVisibilityBuilder.Build(root, eyeOffPlane, lookup, ViewProj(eyeOffPlane, gaze), null, PortalVisibilityBuilder.ShellDrawLiftZ);
+ Assert.DoesNotContain(upperId, fEdge.OrderedVisibleCells); // edge-on drops (mechanism)
+ Assert.Contains(upperId, fOff.OrderedVisibleCells); // off-plane admits (control)
+ }
+
+ ///
+ /// #177 sweep characterization: place the player on the 015F staircase and drive the
+ /// REAL chase camera + collision sweep, reporting where the eye lands relative to the
+ /// 015F ceiling-portal plane (world z=0). If the eye rests AT the plane (|D|~0) the
+ /// flood collapses (see EdgeOnCeilingPortal test); this shows whether that is a swept
+ /// contact leaving the eye on the surface vs free-space boom geometry.
+ ///
+ [Fact]
+ public void StaircaseSweep_EyeClearanceFromCeilingPortal()
+ {
+ var datDir = ResolveDatDir();
+ if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+ var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
+ var engine = BuildHubEngine(dats);
+ var root015F = renderCells[FacilityHub | 0x015Fu];
+ int upIdx = 0; // portal[0]->01C1 (ceiling)
+
+ bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
+ try
+ {
+ CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
+ CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
+
+ // Player on the 015F stairs (world y~-46, ceiling portal at world z~0), swept
+ // at a range of heights + facings (up-stairs = -Y, down-stairs = +Y).
+ foreach (float pz in new[] { -1.0f, -1.5f, -2.0f, -2.5f, -3.0f, -3.5f })
+ foreach (var (fLabel, yaw) in new (string, float)[] { ("up(-Y)", -MathF.PI / 2f), ("down(+Y)", MathF.PI / 2f) })
+ {
+ var playerPos = new Vector3(60.5f, -46f, pz);
+ var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f));
+ var cam = new RetailChaseCamera
+ {
+ CollisionProbe = new PhysicsCameraCollisionProbe(engine),
+ FovY = MathF.PI / 3f, Aspect = 16f / 9f,
+ };
+ for (int i = 0; i < 300; i++)
+ cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
+
+ var eye = cam.Position;
+ uint viewer = cam.ViewerCellId;
+ // D from the eye to 015F's ceiling-portal plane (only meaningful if viewer==015F).
+ float dCeil = PortalPlaneDistance(root015F, upIdx, eye);
+ _out.WriteLine(FormattableString.Invariant(
+ $"playerZ={pz,5:F1} face={fLabel,-8} pcell=0x{pcell & 0xFFFF:X4} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} D->ceilPortal={dCeil,6:F3}"));
+ }
+ }
+ finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
+ }
+
+ [Theory]
+ // spot label, player x, player cell, description
+ [InlineData(90.0f, 0x0178u, "TOP corridor 0178 (approach view)")]
+ [InlineData(100.0f, 0x0183u, "BOTTOM room 0183 (look-back view)")]
+ public void ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label)
+ {
+ var datDir = ResolveDatDir();
+ if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+ var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
+ var engine = BuildHubEngine(dats);
+ Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
+
+ bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
+ float sT = CameraDiagnostics.TranslationStiffness, sR = CameraDiagnostics.RotationStiffness;
+ try
+ {
+ CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
+ CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
+
+ var playerPos = new Vector3(px, -40f, FloorZ(px));
+ uint playerCell = FacilityHub | pcell;
+ _out.WriteLine($"=== {label} @ ({px:F1},-40,{FloorZ(px):F2}) ===");
+ foreach (float dist in new[] { 2.61f, 8.0f })
+ {
+ _out.WriteLine($" -- zoom Distance={dist} --");
+ var cam = new RetailChaseCamera
+ {
+ CollisionProbe = new PhysicsCameraCollisionProbe(engine),
+ FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist,
+ };
+ for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 30f)
+ {
+ float yaw = yawDeg * MathF.PI / 180f;
+ // Settle at this yaw (static player).
+ for (int i = 0; i < 200; i++)
+ cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, playerCell, 0x5000000A);
+ Vector3 eye = cam.Position; uint viewer = cam.ViewerCellId;
+ var vp = cam.View * cam.Projection;
+ if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
+ {
+ var f = PortalVisibilityBuilder.Build(rootCell, eye, lookup, vp, null, PortalVisibilityBuilder.ShellDrawLiftZ);
+ var vis = f.OrderedVisibleCells.OrderBy(v => v).ToList();
+ string flags = $"0178={(vis.Contains(FacilityHub | 0x0178u) ? "Y" : "-")} 0182={(vis.Contains(FacilityHub | 0x0182u) ? "Y" : "-")} 0183={(vis.Contains(FacilityHub | 0x0183u) ? "Y" : "-")} 0181={(vis.Contains(FacilityHub | 0x0181u) ? "Y" : "-")}";
+ _out.WriteLine(FormattableString.Invariant(
+ $" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} {flags} flood={vis.Count} [{CellSet(vis)}]"));
+ }
+ else
+ _out.WriteLine(FormattableString.Invariant($" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer:X8} ROOT-MISSING"));
+ }
+ }
+ }
+ finally
+ {
+ CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA;
+ CameraDiagnostics.TranslationStiffness = sT; CameraDiagnostics.RotationStiffness = sR;
+ }
+ }
+
+ [Fact]
+ public void Descent_RealCameraSweep_StairCellRetention()
+ {
+ var datDir = ResolveDatDir();
+ if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+
+ var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
+ var engine = BuildHubEngine(dats);
+ Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
+
+ bool savedColl = CameraDiagnostics.CollideCamera;
+ bool savedAlign = CameraDiagnostics.AlignToSlope;
+ float savedT = CameraDiagnostics.TranslationStiffness;
+ float savedR = CameraDiagnostics.RotationStiffness;
+ try
+ {
+ CameraDiagnostics.CollideCamera = true;
+ CameraDiagnostics.AlignToSlope = true;
+ CameraDiagnostics.TranslationStiffness = 0.45f;
+ CameraDiagnostics.RotationStiffness = 0.45f;
+
+ var cam = new RetailChaseCamera
+ {
+ CollisionProbe = new PhysicsCameraCollisionProbe(engine),
+ FovY = MathF.PI / 3f,
+ Aspect = 16f / 9f,
+ };
+
+ const float yaw = 0f; // facing +X, descending
+ const float speed = 3.0f; // m/s run
+ const float dt = 1f / 60f;
+ float startX = 88f, endX = 101.5f;
+
+ // Seat the player + settle the camera at the start pose.
+ uint playerCell = FacilityHub | 0x0178u;
+ Vector3 P(float x) => new Vector3(x, -40f, FloorZ(x));
+ for (int i = 0; i < 400; i++)
+ cam.Update(P(startX), yaw, Vector3.Zero, true, Vector3.UnitZ, dt, playerCell, 0x5000000A);
+
+ _out.WriteLine("x playerCell eye=(x,y,z) viewerCell coh flood stairs rootPortalsPastPlane");
+ float x = startX;
+ uint prevViewer = cam.ViewerCellId;
+ int collapses = 0, incoherentFrames = 0;
+ while (x <= endX)
+ {
+ var playerPos = P(x);
+ // Track player membership like production: AdjustPosition carry-forward.
+ var (pc, found) = engine.AdjustPosition(playerCell, playerPos + new Vector3(0, 0, 0.1f));
+ if (found) playerCell = pc;
+
+ cam.Update(playerPos, yaw, new Vector3(speed, 0, 0), true, Vector3.UnitZ, dt, playerCell, 0x5000000A);
+
+ Vector3 eye = cam.Position;
+ uint viewer = cam.ViewerCellId;
+ var viewProj = cam.View * cam.Projection;
+
+ string floodStr, stairs, coh, past;
+ if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
+ {
+ var frame = PortalVisibilityBuilder.Build(
+ rootCell, eye, lookup, viewProj,
+ buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ);
+ var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
+ floodStr = $"{vis.Count,2} [{CellSet(vis)}]";
+ bool ramp = vis.Contains(FacilityHub | 0x0182u);
+ bool lower = vis.Contains(FacilityHub | 0x0183u);
+ stairs = $"r{(ramp ? "Y" : "-")}l{(lower ? "Y" : "-")}";
+
+ // Coherence: is the eye on the interior side of ALL of root's portals?
+ // If not, the eye is outside root -> the crossed portal's subtree drops.
+ var pastList = new List();
+ bool coherent = true;
+ for (int i = 0; i < rootCell.Portals.Count && i < rootCell.ClipPlanes.Count; i++)
+ {
+ if (!EyeInteriorSide(rootCell, i, eye))
+ {
+ coherent = false;
+ pastList.Add($"p{i}->0x{rootCell.Portals[i].OtherCellId:X4}(D={SideD(rootCell, i, eye):F3})");
+ }
+ }
+ coh = coherent ? "IN " : "OUT";
+ past = pastList.Count == 0 ? "-" : string.Join(",", pastList);
+ if (!coherent) incoherentFrames++;
+ if (vis.Count <= 1) collapses++;
+ }
+ else
+ {
+ floodStr = "root-missing/0"; stairs = "--"; coh = "?"; past = $"viewer=0x{viewer:X8}";
+ }
+
+ bool viewerFlip = viewer != prevViewer;
+ prevViewer = viewer;
+ _out.WriteLine(FormattableString.Invariant(
+ $"{x,6:F2} 0x{playerCell:X8} ({eye.X,7:F3},{eye.Y,7:F3},{eye.Z,7:F3}) 0x{viewer:X8} {coh} {floodStr} {stairs} {(viewerFlip ? "FLIP " : "")}{past}"));
+
+ x += speed * dt;
+ }
+ _out.WriteLine($"SUMMARY: collapses(flood<=1)={collapses} incoherentFrames(eye-outside-root)={incoherentFrames}");
+ }
+ finally
+ {
+ CameraDiagnostics.CollideCamera = savedColl;
+ CameraDiagnostics.AlignToSlope = savedAlign;
+ CameraDiagnostics.TranslationStiffness = savedT;
+ CameraDiagnostics.RotationStiffness = savedR;
+ }
+ }
+}
diff --git a/tools/cdb/pview-spiral2.cdb b/tools/cdb/pview-spiral2.cdb
new file mode 100644
index 00000000..5e67d65b
--- /dev/null
+++ b/tools/cdb/pview-spiral2.cdb
@@ -0,0 +1,10 @@
+.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\vigorous-joliot-f0c3ad\retail-spiral2.log
+.sympath C:\Users\erikn\source\repos\acdream\refs
+.symopt+ 0x40
+.reload /f acclient.exe
+r $t0 = 0
+bp acclient!PView::DrawCells "r $t0=@$t0+1; .if (@$t0 < 6000) { .if (@$t0 % 20 == 0) { r $t1 = @@c++(((acclient!PView*)@ecx)->cell_draw_num); .printf \"[DC] h=%d num=%d:\", @$t0, @$t1; .for (r $t3=0; @$t3 < @$t1; r $t3=@$t3+1) { .printf \" %04x\", @@c++(((acclient!PView*)@ecx)->cell_draw_list.data[@$t3]->m_DID.id) & 0xffff }; .printf \" || eye \"; dx acclient!Render::FrameCurrent->viewer.viewpoint }; gc }"
+g
+.echo ===DETACHING===
+qd
+.logclose
diff --git a/tools/cdb/pview-verify.cdb b/tools/cdb/pview-verify.cdb
new file mode 100644
index 00000000..e6e0d408
--- /dev/null
+++ b/tools/cdb/pview-verify.cdb
@@ -0,0 +1,14 @@
+.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\vigorous-joliot-f0c3ad\retail-pview-verify.log
+.sympath C:\Users\erikn\source\repos\acdream\refs
+.symopt+ 0x40
+.reload /f acclient.exe
+.echo ===XVIEW===
+x acclient!PView::DrawCells
+x acclient!PView::ConstructView
+.echo ===DTPVIEW===
+dt acclient!PView cell_draw_num cell_draw_list
+.echo ===DTENVCELL===
+dt acclient!CEnvCell m_DID
+r $t0 = 0
+bp acclient!PView::DrawCells "r $t0=@$t0+1; .echo ===HIT===; dx ((acclient!PView *)@ecx)->cell_draw_num; dx ((acclient!PView *)@ecx)->cell_draw_list.data[0]->m_DID.id; qd"
+g