fix #116 (partial, Ghidra-confirmed): slide_sphere degenerate guard uses F_EPSILON, not EpsilonSq
The user brought up Ghidra; its decompiler (patchmem.gpr, full PDB)
resolved the Binary-Ninja `test ah,5` x87 branch-sign ambiguity that
blocked the desk read. CSphere::slide_sphere (0x00537440) decompiles
cleanly to:
fVar3 = |cross(collisionNormal, contactPlane.N)|²;
if (::F_EPSILON <= fVar3) { // crease exists
... offset = cross * dot(cross,gDelta)/fVar3;
if (|offset|² < ::F_EPSILON) return COLLIDED_TS; // degenerate guard
... add_offset_to_check_pos -> SLID_TS
}
Retail compares the SQUARED magnitudes against F_EPSILON
(0.000199999995 ~= 0.0002 = PhysicsGlobals.EPSILON). Our port compared
against EpsilonSq (0.0002^2 = 4e-8) - a ~5000x too-tight threshold (the
BN pseudo-C rendered the comparison as `test ah,5` after an x87 FCMP,
which is sign-ambiguous; agent reads disagreed). Fixed both comparisons
at TransitionTypes.cs:3098,3105 to EPSILON.
Effect: crease-exists now needs >=0.81 deg between the wall and contact
normals (was 0.011 deg - which routed near-parallel pairs through the
numerically unstable projection); the degenerate guard now hard-stops
slides under ~1.41 cm like retail (was 0.2 mm). Branch POLARITY was
already correct - no change there.
No regression: full physics suite (612) + full Core (1443) green. Not a
register deviation (no row existed; this is an undocumented porting
error corrected to match retail).
This does NOT close #116 - it fixes a tangential constant, not either
reported shape. Ghidra also settled the two shapes' diagnosis (recorded
in ISSUES.md #116 + physics digest):
- Shape-1: our cn=UnitZ default IS retail-faithful (validate_transition
0x0050aa70 has the identical `if (collision_normal_valid==0)
set_collision_normal(UnitZ)`). The real divergence is upstream -
tick-22760 our collision_normal_valid was false where retail's was
true (it recorded the door-face normal). Needs the instrumented
tick-22760 replay.
- Shape-2 (D4 stays skipped, note sharpened): slide_sphere slides
in-frame (SLID_TS) so Z=1.92 is faithful and the D4 Z=2.0 hard-stop
pin is the suspect half; the threshold fix didn't move D4 (real slide,
not degenerate). Needs a cdb trace of an airborne wall hit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
35961f2039
commit
bf18a54369
3 changed files with 60 additions and 11 deletions
|
|
@ -3930,13 +3930,47 @@ retail's viewer-distance smoothing (update_viewer region) before touching.
|
|||
|
||||
## #116 — Slide-response divergence family: near-perpendicular lateral slide lost + first-airborne-frame in-frame slide vs hard stop
|
||||
|
||||
**Status:** OPEN
|
||||
**Status:** OPEN (narrowed) — one Ghidra-confirmed faithfulness fix
|
||||
SHIPPED 2026-06-12; both reported shapes still need a runtime trace.
|
||||
**Severity:** LOW-MEDIUM (over-blocking, never under-blocking — no
|
||||
walk-throughs; feel-level divergence at walls/doors)
|
||||
**Filed:** 2026-06-11 (BR-7 / A6.P4 ship session)
|
||||
**Component:** physics (slide response — `SlideSphere` degenerate-offset
|
||||
guard + first-contact-frame behavior)
|
||||
|
||||
**GHIDRA SESSION 2026-06-12 (the BN branch-sign ambiguity RESOLVED via a
|
||||
second decompiler — Ghidra MCP, patchmem.gpr, full PDB):**
|
||||
- **SHIPPED (faithfulness fix):** `CSphere::slide_sphere` (Ghidra
|
||||
`0x00537440`) compares its SQUARED magnitudes against `::F_EPSILON`
|
||||
(= 0.000199999995 ≈ 0.0002 = `PhysicsGlobals.EPSILON`): `if (::F_EPSILON
|
||||
<= |cross|²)` (crease) and `if (|offset|² < ::F_EPSILON) return
|
||||
COLLIDED_TS` (degenerate guard). Our port compared against `EpsilonSq`
|
||||
(0.0002² = 4e-8) — a ~5000× too-tight threshold (the BN `test ah,5`
|
||||
obscured it). Fixed at `TransitionTypes.cs:3098,3105`; full physics
|
||||
suite (612) + full Core (1443) green, no regression. Crease now needs
|
||||
≥0.81° between normals (was 0.011°); the guard stops slides under
|
||||
~1.41 cm like retail (was 0.2 mm). NOT a register deviation (no row
|
||||
existed — it was an undocumented porting error; the fix matches retail).
|
||||
⚠️ This does NOT fix either reported shape below.
|
||||
- **Shape-1 RE-DIAGNOSED — our `cn=UnitZ` default is RETAIL-FAITHFUL.**
|
||||
Ghidra `validate_transition` (`0x0050aa70`) does exactly our
|
||||
`TransitionTypes.cs:3701-3702`: `if (collision_normal_valid == 0)
|
||||
set_collision_normal(UnitZ)`. So the harness `cn=(0,0,1)` is the
|
||||
faithful FALLBACK; the real divergence is UPSTREAM — at tick-22760 our
|
||||
`collision_normal_valid` was FALSE (→ UnitZ) where retail's was TRUE
|
||||
(it had recorded the door-face normal `(0,+1,0)`). The bug is in the
|
||||
COLLISION-RECORDING path (find_collisions / collide_with_environment),
|
||||
not slide/validate. Next: replay tick-22760
|
||||
(`DoorBugTrajectoryReplayTests`) instrumented to see where our
|
||||
collision-normal recording drops the wall normal.
|
||||
- **Shape-2 NARROWED — D4 stays skipped.** Ghidra confirms slide_sphere
|
||||
applies the slide IN-FRAME (`add_offset_to_check_pos` → SLID_TS), so our
|
||||
Z=1.92 is faithful TO slide_sphere and the D4 Z=2.0 hard-stop pin is the
|
||||
SUSPECT half. But the threshold fix did NOT change D4 (its offset is a
|
||||
real slide, not degenerate), so whether retail's first airborne frame
|
||||
REACHES slide_sphere (→1.92) or hard-stops upstream still needs a cdb
|
||||
trace of an airborne wall hit before flipping the assertion.
|
||||
|
||||
**Two pinned shapes, both pre-dating BR-7 (the per-cell shadow port left
|
||||
them byte-identical):**
|
||||
|
||||
|
|
|
|||
|
|
@ -3095,14 +3095,26 @@ public sealed class Transition
|
|||
Vector3 direction = Vector3.Cross(collisionNormal, contactPlane.Normal);
|
||||
float dirLenSq = direction.LengthSquared();
|
||||
|
||||
if (dirLenSq >= PhysicsGlobals.EpsilonSq)
|
||||
// #116 (2026-06-12, Ghidra-confirmed): retail CSphere::slide_sphere
|
||||
// (0x00537440) compares these SQUARED magnitudes against F_EPSILON
|
||||
// (0.000199999995 ≈ 0.0002 = PhysicsGlobals.EPSILON), NOT against the
|
||||
// squared epsilon. Ghidra decomp: `if (::F_EPSILON <= fVar3)` where
|
||||
// fVar3 = |cross|², and `if (|offset|² < ::F_EPSILON) return
|
||||
// COLLIDED_TS`. Our port used EpsilonSq (0.0002² = 4e-8) — a ~5000×
|
||||
// too-tight threshold (the BN pseudo-C `test ah,5` branch obscured the
|
||||
// constant; the Ghidra second-decompiler pass settled it). Effect:
|
||||
// crease-exists now needs ≥0.81° between the normals (was 0.011°,
|
||||
// routing near-parallel pairs through the unstable projection); the
|
||||
// degenerate guard now stops slides under ~1.41 cm like retail (was
|
||||
// 0.2 mm). Register: AP-? (divergence retired). See ISSUES.md #116.
|
||||
if (dirLenSq >= PhysicsGlobals.EPSILON)
|
||||
{
|
||||
// Crease exists: project displacement onto it.
|
||||
float diff = Vector3.Dot(direction, gDelta);
|
||||
float invDirLenSq = 1f / dirLenSq;
|
||||
Vector3 offset = direction * diff * invDirLenSq;
|
||||
|
||||
if (offset.LengthSquared() < PhysicsGlobals.EpsilonSq)
|
||||
if (offset.LengthSquared() < PhysicsGlobals.EPSILON)
|
||||
return TransitionState.Collided;
|
||||
|
||||
// Subtract current displacement to get the correction vector.
|
||||
|
|
|
|||
|
|
@ -546,14 +546,17 @@ public class BSPStepUpTests
|
|||
/// every frame replays the same hard stop and the character hangs in falling
|
||||
/// animation until another correction breaks the loop.
|
||||
/// </summary>
|
||||
[Fact(Skip = "Issue #116 — slide-response divergence family (P1-era " +
|
||||
"slide_sphere work made the first airborne wall frame slide in-frame " +
|
||||
"to Z=1.92 instead of the L.2c-pinned hard stop at Z=2.0; the cached " +
|
||||
"sliding-normal mechanism retail seeds via get_object_info " +
|
||||
"(pc:279992, transient bit 4 → init_sliding_normal) only governs the " +
|
||||
"NEXT frame, so which first-frame response is retail-faithful needs " +
|
||||
"its own oracle read. NOT a cell-set problem — BR-7/A6.P4 left this " +
|
||||
"byte-identical. See docs/ISSUES.md #116.")]
|
||||
[Fact(Skip = "Issue #116 shape-2 — the engine slides IN-FRAME to Z=1.92 " +
|
||||
"on the first airborne wall frame; this pin expects an L.2c hard stop " +
|
||||
"at Z=2.0. Ghidra (2026-06-12) confirms retail CSphere::slide_sphere " +
|
||||
"(0x00537440) applies the slide IN-FRAME (add_offset_to_check_pos → " +
|
||||
"SLID_TS), so our 1.92 is faithful TO slide_sphere and the Z=2.0 " +
|
||||
"expectation is the SUSPECT half — but whether retail's first " +
|
||||
"airborne frame REACHES slide_sphere (→1.92) or hard-stops upstream " +
|
||||
"(collide_with_environment dispatch / no last-known plane) needs a " +
|
||||
"cdb trace of an airborne wall hit before flipping the assertion. The " +
|
||||
"#116 threshold fix (EpsilonSq→F_EPSILON) did NOT change this — the D4 " +
|
||||
"offset is a real slide, not degenerate. See docs/ISSUES.md #116.")]
|
||||
public void D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames()
|
||||
{
|
||||
var (root, resolved) = BSPStepUpFixtures.TallWall();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue