test #137: the seam shake reproduced offline — third mechanism characterized

The corridor gate FAILED with a changed symptom: shaking at cell seams
(+ purple floor flashing there) instead of the dead stop. Deep-probe
session (step-walk/push-back/indoor-bsp + full resolve capture) traced
the complete chain; docs/ISSUES.md #137 carries it. Short form:

- Corridor floors are double-faced portal slabs over under-rooms; the
  resting foot sphere lives within half a millimeter of three hit/straddle
  thresholds there.
- Crossing a boundary, the foot penetrates the neighbor ramp slab by
  ~0.4mm, steps up onto it successfully (+0.6mm lift, stepped=True) —
  and the lifted check position is then LOST: the following pass runs at
  the unlifted height (the P2 stale-snapshot class; retail step_up
  0x0050b6cc restores only on FAILURE).
- The unlifted re-test grazes the under-room's ceiling (the slab
  underside) within the near-miss window, dispatches a neg-poly step-up
  with a DOWNWARD normal, whose nested step-down finds no walkable at
  exact tangency -> StepUpSlide -> slide_sphere opposing branch ->
  reversed-movement collision -> Collided -> revert. Every frame = shake.

Apparatus committed:
- Issue137CorridorSeamReplayTests: 3 deterministic offline repros
  (snapshot-exact west-boundary from the capture, east deep-straddle,
  the clean-run pin), currently Skip='#137 seam shake' pending the fix.
  Key: THREE portal-ring hydration (the under-room 0x8A020166 is ring-3;
  with fewer rings the flood can't add it and the bug vanishes) + live
  Setup step heights (0.6/1.5) + probe-buffer capture for line-diffing
  offline vs live traces.
- Issue137CorridorSeamInspectionTests: portal-poly world spans (exposed
  the visual-vs-physics polygon-id conflation and the floor-portal
  topology), physics-BSP leaf membership walk, hit-normal candidate
  sweep (|align| both windings), downward-poly sweep.

NEXT: read TransitionalInsert's attempt loop against retail 0x0050b6f0,
find the restore that clobbers the successful step-up position, fix,
un-skip. The purple seam flashing is expected to be the render exposing
the same per-frame oscillation - re-check after the physics fix.

Suites: Core 2553 / App 713 / UI 425 / Net 385, 0 failures (5 skips =
2 pre-existing + the 3 parked repros).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-05 19:05:19 +02:00
parent e8651b3819
commit dbddad7a5e
4 changed files with 25131 additions and 4 deletions

View file

@ -1183,8 +1183,46 @@ walls** in particular. (Symptoms not fully characterized yet: likely walking thr
openings that should block / blocking at openings that should pass, and door collision not openings that should block / blocking at openings that should pass, and door collision not
matching the door's open/closed state.) matching the door's open/closed state.)
**CORRIDOR PHANTOM FIXED 2026-07-06 (both mechanisms resolved; visual gate **GATE 2026-07-06 FAILED — THIRD MECHANISM CHARACTERIZED (the seam shake),
pending — see `docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md` deterministic offline repro secured:** with mechanisms 1+2 fixed the dead
stop became a SHAKE at cell seams (+ purple floor flashing there — almost
certainly the render exposing the same per-frame position/OnWalkable
oscillation; re-check after the physics fix). Full chain, every link
probe-traced (`launch-137-seam-probes.log`, capture
`resolve-137-seam-capture.jsonl` tick 4101 ×46):
1. Corridor cells sit above under-rooms; the shared floor slab is
double-faced (up-face + underside as separate physics polys) and IS a
portal plane (e.g. 0x8A020165's ramp over 0x8A020166). The resting foot
sphere is permanently within ±0.5 mm of THREE thresholds there (poly-hit
rε, walkable rε, portal-straddle r+ε).
2. Walking across the boundary at the flat-floor height penetrates the
ramp slab by ~0.4 mm → foot full-hit on the up-face → StepSphereUp →
step-down accepts the ramp (+0.6 mm lift, CheckPos 5.999,
`[stepsphereup] stepped=True`).
3. **THE BUG: the lifted position is then LOST** — the next pass runs at
the UNLIFTED height (GlobalSphere center 5.520 vs the lifted 5.519;
the P2 stale-snapshot class, single-slot Save/RestoreCheckPos clobber
suspected — retail `CTransition::step_up` 0x0050b6cc restores ONLY on
failure) → the re-test at 0.4 mm inside the slab grazes the NEIGHBOR
under-room's CEILING (the slab underside, n≈(0.03,0,1)) within the
near-miss window → recorded (retail records it too — pos_hits_sphere
registers geometric hits pre-cull) → neg-poly step-up dispatch with the
DOWNWARD normal → the nested step-down finds no walkable at exact
tangency → StepUpSlide → slide_sphere(down normal vs up contact plane)
→ the opposing branch → reversed-movement collision normal → Collided →
validate revert (Contact/OnWalkable stripped) → next step's AdjustOffset
zeroes → out==in every frame = the shake. Retail never enters at step 3:
its kept step-up lift leaves the sphere ON the surface, no graze.
4. Offline repro: `Issue137CorridorSeamReplayTests` (3 tests, currently
`Skip="#137 seam shake"`) reproduce the block deterministically — the
key was hydrating THREE portal rings (the under-room 0x8A020166 is
ring-3; with fewer rings the flood can't add it and everything passes).
NEXT: read our `TransitionalInsert` attempt loop against retail
0x0050b6f0 to find the restore that clobbers the successful step-up's
position; fix; un-skip the three tests.
**CORRIDOR PHANTOM mechanisms 1+2 FIXED 2026-07-06 (see
`docs/research/2026-07-06-137-sliding-normal-lifecycle-audit.md`
for the full audit):** mechanism 2 = BSPQuery Contact-branch stub slide for the full audit):** mechanism 2 = BSPQuery Contact-branch stub slide
responses leaked sliding normals retail's BSP layer never writes (fixed: responses leaked sliding normals retail's BSP layer never writes (fixed:
real `slide_sphere` routing + success-gated body writeback). Mechanism 1 as real `slide_sphere` routing + success-gated body writeback). Mechanism 1 as

24868
resolve-137-seam-capture.jsonl Normal file

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,7 @@ public class Issue137CorridorSeamInspectionTests
[Theory] [Theory]
[InlineData(0x8A02016Eu)] [InlineData(0x8A02016Eu)]
[InlineData(0x8A02017Au)] [InlineData(0x8A02017Au)]
[InlineData(0x8A02011Eu)] // the under-floor room the corridor's floor-portals lead to
public void CorridorCell_PhysicsPolysAndPortals_DatInspection(uint envCellId) public void CorridorCell_PhysicsPolysAndPortals_DatInspection(uint envCellId)
{ {
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR") var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
@ -254,7 +255,10 @@ public class Issue137CorridorSeamInspectionTests
System.Numerics.Vector3.Cross(w1 - w0, w2 - w0)); System.Numerics.Vector3.Cross(w1 - w0, w2 - w0));
float align = System.Numerics.Vector3.Dot(n, hitNormal); float align = System.Numerics.Vector3.Dot(n, hitNormal);
if (align < 0.95f) continue; // within ~18° of the recorded normal // |align|: the vertex-fan winding convention can flip the
// computed normal vs the physics plane's true facing — accept
// both signs (2026-07-06 sweep flaw fix).
if (MathF.Abs(align) < 0.95f) continue; // within ~18° of the recorded normal
// Plane distance from the hit point. // Plane distance from the hit point.
float d = -System.Numerics.Vector3.Dot(n, w0); float d = -System.Numerics.Vector3.Dot(n, w0);
@ -358,4 +362,67 @@ public class Issue137CorridorSeamInspectionTests
} }
_out.WriteLine("(downward sweep complete)"); _out.WriteLine("(downward sweep complete)");
} }
/// <summary>
/// 2026-07-06 gate-session follow-up: seam crossings SUCCEED at
/// y≈40.8..41.2 and BLOCK at y≈39.5..39.8 (cell-transit log,
/// launch-137-corridor-gate.log). A y-dependent boundary with no physics
/// polygon culprit points at the PORTAL POLYGONS — if the doorway
/// openings don't span the full corridor width, the transit/membership
/// machinery only hands the sphere to the neighbor inside the portal
/// poly's span. Dump every portal polygon's world-space vertex extent.
/// </summary>
[Theory]
[InlineData(0x8A02016Eu)]
[InlineData(0x8A02017Au)]
public void CorridorCell_PortalPolygonWorldSpans(uint envCellId)
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
if (!Directory.Exists(datDir))
{
_out.WriteLine($"SKIP: dat directory not found at {datDir}");
return;
}
using var dats = new DatCollection(datDir, DatAccessType.Read);
var envCell = dats.Get<EnvCell>(envCellId);
Assert.NotNull(envCell);
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(0x0D000000u | envCell!.EnvironmentId);
Assert.NotNull(environment);
Assert.True(environment!.Cells.TryGetValue(envCell.CellStructure, out var cs));
var rot = new System.Numerics.Quaternion(
envCell.Position.Orientation.X, envCell.Position.Orientation.Y,
envCell.Position.Orientation.Z, envCell.Position.Orientation.W);
var world = System.Numerics.Matrix4x4.CreateFromQuaternion(rot)
* System.Numerics.Matrix4x4.CreateTranslation(
envCell.Position.Origin.X, envCell.Position.Origin.Y, envCell.Position.Origin.Z);
_out.WriteLine($"=== 0x{envCellId:X8} portal polygons (world spans) ===");
foreach (var p in envCell.CellPortals)
{
if (!cs!.Polygons.TryGetValue((ushort)p.PolygonId, out var poly))
{
_out.WriteLine($" portal poly {p.PolygonId} -> 0x{p.OtherCellId:X4} {p.Flags}: NOT in visual set");
continue;
}
var min = new System.Numerics.Vector3(float.MaxValue);
var max = new System.Numerics.Vector3(float.MinValue);
foreach (var vid in poly.VertexIds)
{
if (!cs.VertexArray.Vertices.TryGetValue((ushort)vid, out var v)) continue;
var w = System.Numerics.Vector3.Transform(v.Origin, world);
min = System.Numerics.Vector3.Min(min, w);
max = System.Numerics.Vector3.Max(max, w);
}
_out.WriteLine(
$" portal poly {p.PolygonId} -> 0x{p.OtherCellId:X4} [{p.Flags}] " +
$"x=[{min.X:F2},{max.X:F2}] y=[{min.Y:F2},{max.Y:F2}] z=[{min.Z:F2},{max.Z:F2}] " +
$"verts={poly.VertexIds.Count}");
}
}
} }

View file

@ -73,6 +73,22 @@ public class Issue137CorridorSeamReplayTests
toLoad.Add(0x8A020000u | p.OtherCellId); toLoad.Add(0x8A020000u | p.OtherCellId);
} }
// Expand three portal rings — the live collision cell array reaches
// cells three hops out (0x8A020166, the under-ramp room whose ceiling
// is the ramp slab's underside, is ring-3 in the 2026-07-06
// seam-shake trace; with only two rings the offline flood can never
// add it and the shake does not reproduce).
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) foreach (var cellId in toLoad)
{ {
var envCell = dats.Get<EnvCell>(cellId); var envCell = dats.Get<EnvCell>(cellId);
@ -131,7 +147,145 @@ public class Issue137CorridorSeamReplayTests
body: body, body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide); moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide);
[Fact] /// <summary>
/// 2026-07-06 seam-shake repro, snapshot-exact (probe session
/// launch-137-seam-probes.log + resolve-137-seam-capture.jsonl tick 4101,
/// repeated ×46): running WEST across the x=75 boundary
/// (0x8A02016E → 0x8A020165, the ramp cell) from (75.287, 40.035, 6)
/// toward (74.685, 39.988, 6), the resolve blocks with the SYNTHETIC
/// reversed-movement normal (0.997, 0.078, 0.002) and out==in — every
/// frame — the "shaking at the seam" report.
///
/// <para>
/// Probe-traced chain: the foot sphere (tangent to the floor) crosses
/// onto 0165's ramp floor; the ramp slab is double-faced and the
/// UNDERSIDE face (poly 0, n=(0.03,0,1)) grazes the sphere within the
/// hit threshold → recorded as a foot near-miss → neg-poly step-up
/// dispatch with a downward normal → the nested step-up's walkable probe
/// rejects the exactly-tangent ramp floor ([walkable-nearest]
/// gap=0.0000 overlapsSphere=False) → StepUpSlide →
/// slide_sphere(downward normal vs up-facing contact plane) → the
/// opposing-normals branch → Collided → revert. Repeat.
/// </para>
/// </summary>
[Fact(Skip = "#137 seam shake — deterministic RED repro; the post-step-up check-pos restore clobbers the lifted position (P2 stale-snapshot class). Un-skip with the fix.")]
public void SeamShake_WestBoundary_SnapshotExact_Advances()
{
var datDir = FindDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: dat directory not found");
return;
}
using var dats = new DatCollection(datDir, DatAccessType.Read);
var engine = BuildCorridorEngine(dats);
// Body seeded EXACTLY from the capture's bodyBefore (tick 4101).
var body = new PhysicsBody();
body.ContactPlaneValid = true;
body.ContactPlane = new Plane(Vector3.UnitZ, 6f);
body.ContactPlaneCellId = SeamCellWest;
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, -38.33333f, -6f),
new Vector3(75f, -41.66667f, -6f),
new Vector3(78.33333f, -41.66667f, -6f),
new Vector3(78.33333f, -38.33333f, -6f),
};
var from = new Vector3(75.28674f, -40.03537f, -6f);
var to = new Vector3(74.6854f, -39.988018f, -6f);
// Emit the same step-level probes the live session logged so the
// offline trace can be line-diffed against launch-137-seam-probes.log
// — the first divergent line names the state the replay is missing.
var probeBuffer = new System.IO.StringWriter();
var prevOut = Console.Out;
ResolveResult r1;
try
{
Console.SetOut(probeBuffer);
PhysicsDiagnostics.ProbeStepWalkEnabled = true;
PhysicsDiagnostics.ProbePushBackEnabled = true;
PhysicsDiagnostics.ProbeIndoorBspEnabled = true;
r1 = engine.ResolveWithTransition(
currentPos: from,
targetPos: to,
cellId: SeamCellWest,
sphereRadius: 0.48f,
sphereHeight: 1.2f,
stepUpHeight: 0.6f, // live Setup values from the capture
stepDownHeight: 1.5f,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide);
}
finally
{
PhysicsDiagnostics.ProbeStepWalkEnabled = false;
PhysicsDiagnostics.ProbePushBackEnabled = false;
PhysicsDiagnostics.ProbeIndoorBspEnabled = false;
Console.SetOut(prevOut);
}
_out.WriteLine(probeBuffer.ToString());
_out.WriteLine($"r1: ok={r1.Ok} out=({r1.Position.X:F3},{r1.Position.Y:F3},{r1.Position.Z:F3}) " +
$"cell=0x{r1.CellId:X8} hit={r1.CollisionNormalValid} " +
$"n=({r1.CollisionNormal.X:F2},{r1.CollisionNormal.Y:F2},{r1.CollisionNormal.Z:F2}) " +
$"bodySliding={body.TransientState.HasFlag(TransientStateFlags.Sliding)}");
Assert.True(r1.Position.X < from.X - 0.3f,
$"The westward boundary crossing onto the ramp must advance " +
$"({from.X:F3} → {r1.Position.X:F3}, target {to.X:F3}); zero " +
$"advance with the reversed-movement normal = the seam shake.");
}
/// <summary>
/// 2026-07-06 gate session repro (launch-137-corridor-gate.log): standing
/// at (84.851, 39.764, 6.000) — the foot sphere already straddling the
/// x=85 cell boundary by 0.33 m — the first move attempt toward
/// (85.453, 39.782) blocked with the synthetic reversed-movement normal
/// (1.00, 0.03, 0.02), out==in, cp lost (cp=none), and repeated every
/// frame (the "shaking at the seam" report). The deeper straddle start is
/// what the original replay frame (84.638 → 85.253) didn't cover.
/// </summary>
[Fact(Skip = "#137 seam shake — deterministic RED repro; the post-step-up check-pos restore clobbers the lifted position (P2 stale-snapshot class). Un-skip with the fix.")]
public void SeamCrossing_FromDeepStraddleStart_Advances()
{
var datDir = FindDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: dat directory not found");
return;
}
using var dats = new DatCollection(datDir, DatAccessType.Read);
var engine = BuildCorridorEngine(dats);
var body = GroundedBody();
var from = new Vector3(84.851f, -39.764f, -6.000f);
var to = new Vector3(85.453f, -39.782f, -6.000f);
var r1 = Resolve(engine, body, from, to, SeamCellWest);
_out.WriteLine($"r1: ok={r1.Ok} out=({r1.Position.X:F3},{r1.Position.Y:F3},{r1.Position.Z:F3}) " +
$"cell=0x{r1.CellId:X8} hit={r1.CollisionNormalValid} " +
$"n=({r1.CollisionNormal.X:F2},{r1.CollisionNormal.Y:F2},{r1.CollisionNormal.Z:F2}) " +
$"bodySliding={body.TransientState.HasFlag(TransientStateFlags.Sliding)} " +
$"bodyCpValid={body.ContactPlaneValid}");
Assert.True(r1.Position.X > from.X + 0.2f,
$"The straddling-start seam crossing must advance " +
$"({from.X:F3} → {r1.Position.X:F3}); zero advance with a " +
$"reversed-movement normal = the 2026-07-06 seam shake.");
}
[Fact(Skip = "#137 seam shake — deterministic RED repro; the post-step-up check-pos restore clobbers the lifted position (P2 stale-snapshot class). Un-skip with the fix.")]
public void SeamCrossing_DoesNotPersistSyntheticSlidingNormal_AndRunContinues() public void SeamCrossing_DoesNotPersistSyntheticSlidingNormal_AndRunContinues()
{ {
var datDir = FindDatDir(); var datDir = FindDatDir();