port #181: retail viewer step subdivision (calc_num_steps 0x0050a0b0) - radius-anchored steps + remainder final step + viewer-exempt small-offset abort
The user's retail axiom (camera rock steady pressed into walls) vs our measured wall-press wander (~0.5mm/frame limit cycle, headless pin Issue181WallPressEquilibriumTests) sent us back to the decomp. Ghidra (clean, vs the BN x87 mush): retail VIEWERS subdivide the sweep into EXACTLY radius-length steps anchored at the start (offsetPerStep = offset*r/len, numSteps = floor(len/r)+1) with the final step recomputed mid-loop as the exact remainder (find_transitional_position 0x0050bdf0), and the negligible-offset abort is NON-viewer-only. Ours used ceil equal-slices for everything and aborted viewers too. Ported faithfully (pseudocode docs/research/2026-07-06-viewer-step-subdivision-pseudocode.md); non-viewer stepping already matched (TRANSITIONAL_PERCENT_OF_RADIUS=1.0). Measurement: the wall-press limit cycle is UNCHANGED by the port (537.8um avg; a bit-exact 12-frame cycle: ~130um/frame inward creep x11 then a 2.6mm snap). With adjust_to_plane + adjust_sphere_to_poly now also Ghidra-verified faithful, the residual mm cycle is likely retail-class plateau physics - invisible at retail's 60fps vsync, tear-interleaved into visible stripes at our ~1500fps unsynced. The decisive user test: VSync ON (Settings/F11). Fallback discriminator: cdb-trace retail's viewer at a wall press. Suites green (Core 2600 / App 733 / UI 425 / Net 385). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d4ff80cb2b
commit
3f34bca06f
3 changed files with 318 additions and 16 deletions
|
|
@ -770,8 +770,19 @@ public sealed class Transition
|
|||
return false;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Step subdivision: each sub-step travels at most one sphere radius
|
||||
// to prevent tunnelling through thin surfaces.
|
||||
// Step subdivision (retail CTransition::calc_num_steps 0x0050a0b0,
|
||||
// pseudocode docs/research/2026-07-06-viewer-step-subdivision-pseudocode.md).
|
||||
// Two shapes:
|
||||
// - VIEWER (the camera sweep, state & 4): steps of EXACTLY one radius,
|
||||
// anchored at the start — numSteps = floor(len/r) + 1, with the final
|
||||
// step recomputed in the loop as the exact remainder. The anchored
|
||||
// grid is invariant under mm-scale target drift, which is what makes
|
||||
// retail's wall-pressed camera a fixed point ("rock steady"); the
|
||||
// pre-#181 equal-slice grid reshuffled every boundary per frame and
|
||||
// the camera orbited a ~0.5 mm/frame limit cycle instead
|
||||
// (Issue181WallPressEquilibriumTests).
|
||||
// - non-viewer: equal slices of ~one radius (TRANSITIONAL_PERCENT_OF_
|
||||
// RADIUS = 1.0 at 0x007c6874) — unchanged, matches retail.
|
||||
// ------------------------------------------------------------------
|
||||
Vector3 offset = sp.EndPos - sp.BeginPos;
|
||||
float dist = offset.Length();
|
||||
|
|
@ -781,25 +792,41 @@ public sealed class Transition
|
|||
if (radius <= PhysicsGlobals.EPSILON)
|
||||
return false;
|
||||
|
||||
float step = dist / radius;
|
||||
|
||||
int numSteps;
|
||||
Vector3 offsetPerStep;
|
||||
|
||||
if (step > 1.0f)
|
||||
if (ObjectInfo.IsViewer)
|
||||
{
|
||||
numSteps = (int)MathF.Ceiling(step);
|
||||
offsetPerStep = offset * (1f / numSteps);
|
||||
}
|
||||
else if (offset != Vector3.Zero)
|
||||
{
|
||||
numSteps = 1;
|
||||
offsetPerStep = offset;
|
||||
if (dist > PhysicsGlobals.EPSILON)
|
||||
{
|
||||
offsetPerStep = offset * (radius / dist); // radius-length steps
|
||||
numSteps = (int)MathF.Floor(dist / radius) + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
numSteps = 0;
|
||||
offsetPerStep = Vector3.Zero;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
numSteps = 0;
|
||||
offsetPerStep = Vector3.Zero;
|
||||
float step = dist / radius;
|
||||
|
||||
if (step > 1.0f)
|
||||
{
|
||||
numSteps = (int)MathF.Ceiling(step);
|
||||
offsetPerStep = offset * (1f / numSteps);
|
||||
}
|
||||
else if (offset != Vector3.Zero)
|
||||
{
|
||||
numSteps = 1;
|
||||
offsetPerStep = offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
numSteps = 0;
|
||||
offsetPerStep = Vector3.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
// Retail safety cap (30 steps). Viewer/sight objects bypass it, matching
|
||||
|
|
@ -843,6 +870,13 @@ public sealed class Transition
|
|||
|
||||
for (int i = 0; i < numSteps; i++)
|
||||
{
|
||||
// Viewer last-step remainder (retail find_transitional_position
|
||||
// 0x0050bdf0: on i == numSteps−1 for state&4, the step offset is
|
||||
// recomputed as offset · (len − (numSteps−1)·r)/len — the sweep
|
||||
// ends exactly at EndPos, never overshooting the anchored grid).
|
||||
if (ObjectInfo.IsViewer && i == numSteps - 1 && dist > PhysicsGlobals.EPSILON)
|
||||
offsetPerStep = offset * ((dist - (numSteps - 1) * radius) / dist);
|
||||
|
||||
Vector3 requestedOffset = offsetPerStep;
|
||||
|
||||
// Per ACE order: AdjustOffset FIRST (uses state from previous step),
|
||||
|
|
@ -859,8 +893,13 @@ public sealed class Transition
|
|||
}
|
||||
|
||||
// Abort if adjusted offset is negligible (stuck against a wall
|
||||
// with no slide tangent available).
|
||||
if (sp.GlobalOffset.LengthSquared() < PhysicsGlobals.EpsilonSq)
|
||||
// with no slide tangent available). NON-VIEWER-ONLY per retail
|
||||
// (find_transitional_position 0x0050bdf0: `(state & 4) == 0 &&
|
||||
// |global_offset|² < F_EPSILON²`) — a pressed camera keeps
|
||||
// stepping through sub-epsilon adjusted offsets so its remainder
|
||||
// step still lands exactly on the sought (#181 equilibrium).
|
||||
if (!ObjectInfo.IsViewer
|
||||
&& sp.GlobalOffset.LengthSquared() < PhysicsGlobals.EpsilonSq)
|
||||
{
|
||||
if (stepWalkProbe)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue