mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Fix walking NaNs from subnormal velocity delta refinement
This commit is contained in:
1 parent
dff672aee4
commit
f33d60a3a6
4 files changed
+109
-1
No files matched your search
@@ -86,6 +86,20 @@ fn constructor_uses_clip_metrics_and_original_initial_fields() {
|
||||
assert_eq!(s.motion.target_scale_784, -1.0);
|
||||
assert_eq!(s.cadence.phase.duration, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_velocity_delta_keeps_ground_publication_finite() {
|
||||
let mut controller = Controller::new(settings(), metrics());
|
||||
controller.state.motion.velocity_480 = [0.0, 1.0e-21, 1.0, 0.0];
|
||||
controller.state.motion.speed_704 = 1.0;
|
||||
let result = controller.step_ground(&job());
|
||||
assert!(result.position.into_iter().all(f32::is_finite));
|
||||
assert!(result.velocity.into_iter().all(f32::is_finite));
|
||||
assert!(result.physical_frame.into_iter().flatten().all(f32::is_finite));
|
||||
assert!(result.animation_frame.into_iter().flatten().all(f32::is_finite));
|
||||
assert!(controller.state.surface.lean.into_iter().all(f32::is_finite));
|
||||
assert!(controller.state.cadence.phase.phase.is_finite());
|
||||
}
|
||||
#[test]
|
||||
fn absent_clip_queries_have_original_zero_speed() {
|
||||
let s = State::new([None; 3]);
|
||||
|
||||
@@ -10,13 +10,22 @@ pub(super) fn scale(a: Vector, s: f32) -> Vector { a.map(|v| v * s) }
|
||||
pub(super) fn sub(a: Vector, b: Vector) -> Vector { std::array::from_fn(|i| a[i] - b[i]) }
|
||||
pub(super) fn madd(a: Vector, s: f32, b: Vector) -> Vector { std::array::from_fn(|i| a[i].mul_add(s, b[i])) }
|
||||
fn inverse_squared(value: f32) -> f32 {
|
||||
// The portable estimate preserves subnormals. For a tiny positive squared
|
||||
// length, r*r can overflow even though both the length and inverse root
|
||||
// are finite. Normalize its exponent before the recovered refinements;
|
||||
// rsqrt(x * 2^24) * 2^12 = rsqrt(x). Normal inputs keep their old path.
|
||||
let (value, rescale) = if value > 0.0 && value.is_subnormal() {
|
||||
(value * 16_777_216.0, 4096.0)
|
||||
} else {
|
||||
(value, 1.0)
|
||||
};
|
||||
//PC seed is not bit-exact Xenon; retain both original refinement stages.
|
||||
let mut r = crate::physics::reciprocal_sqrt::estimate(value);
|
||||
for _ in 0..2 {
|
||||
let error = (-value).mul_add(r * r, 1.0);
|
||||
r = (r * 0.5).mul_add(error, r);
|
||||
}
|
||||
r
|
||||
r * rescale
|
||||
}
|
||||
pub(super) fn length(v: Vector) -> f32 {
|
||||
let squared = dot(v, v);
|
||||
|
||||
@@ -1,4 +1,49 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tiny_velocity_delta_remains_finite_and_reaches_target() {
|
||||
// Flat-ground convergence can leave a tiny vertical correction while X/Z
|
||||
// already match. Its squared length is positive but subnormal.
|
||||
let current = [-1.6481168, 1.0e-21, 5.7692037, 0.0];
|
||||
let target = [-1.6481168, 0.0, 5.7692037, 0.0];
|
||||
let delta = sub(target, current);
|
||||
assert!(dot(delta, delta).is_subnormal());
|
||||
let result = limit_delta(target, current, 0.2);
|
||||
assert_eq!(result, target);
|
||||
assert!(length(delta).is_finite());
|
||||
assert!((length(delta) / 1.0e-21 - 1.0).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_velocity_refinement_boundaries_preserve_limits_and_invalid_inputs() {
|
||||
for exponent in -74..=-60 {
|
||||
let component = 2.0f32.powi(exponent);
|
||||
let delta = [component, 0.0, 0.0, 0.0];
|
||||
let measured = length(delta);
|
||||
assert!(measured.is_finite() && measured > 0.0);
|
||||
assert!((measured / component - 1.0).abs() < 1e-6);
|
||||
let maximum = component * 0.5;
|
||||
let limited = limit_delta(delta, [0.0; 4], maximum);
|
||||
assert!(limited.into_iter().all(f32::is_finite));
|
||||
assert!((limited[0] / maximum - 1.0).abs() < 1e-6);
|
||||
}
|
||||
assert_eq!(length([0.0; 4]), 0.0);
|
||||
assert!(length([f32::NAN, 0.0, 0.0, 0.0]).is_nan());
|
||||
assert!(limit_delta([f32::NAN, 0.0, 0.0, 0.0], [0.0; 4], 0.2)[0].is_nan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_velocity_correction_does_not_poison_walking_state() {
|
||||
let mut state = State { velocity: [0.0, 1.0e-21, 5.0, 0.0], speed: 5.0, ..State::default() };
|
||||
let mut input = input();
|
||||
input.steering = 0.0;
|
||||
for _ in 0..120 {
|
||||
state.update(&settings(), &input);
|
||||
assert_eq!(state.velocity, [0.0, 0.0, 5.0, 0.0]);
|
||||
assert_eq!(state.speed, 5.0);
|
||||
assert!(state.forward_delta.is_finite() && state.right_delta.is_finite());
|
||||
}
|
||||
}
|
||||
fn curve(value: f32) -> PointGraph<8> { PointGraph { x: std::array::from_fn(|i| i as f32), y: [value; 8] } }
|
||||
fn settings() -> Settings {
|
||||
Settings { slope_speed_scalar: curve(1.0), slope_mode_speed: curve(3.0), turn_vs_speed: curve(90.0), turn_delta_vs_speed: curve(15.0) }
|
||||
|
||||
@@ -12,6 +12,46 @@ Coverage limits: a Windows unhandled-exception filter attempts a fixed, allocati
|
||||
|
||||
For manual UI validation, run the executable with `--crash-report-preview`. This generates a clearly synthetic report and opens the popup without initializing the game, setup, Steam or assets. Check Copy report, clipboard text, Open folder, saved UTF-8 text and Close. Static tests do not validate desktop presentation.
|
||||
|
||||
## Walking velocity convergence (2026-09-09)
|
||||
|
||||
Observed user failure: clean revision `dff672a`, build timestamp
|
||||
`1788969088174193000`, report timestamp `1788969335`. At tick4059 in
|
||||
BipedGround on University, `OFFBOARD_GROUND_RESULT_NONFINITE` precedes
|
||||
`Invalid offboard swept-line request`. The job has finite spatial inputs;
|
||||
the controller publishes NaN velocity/position, a NaN right delta and a
|
||||
forward delta of approximately `-3.11e-43`. The collision query then rejects
|
||||
NaN XYZ coordinates. Its validation is correct and remains intact.
|
||||
|
||||
The walking velocity limiter's portable reciprocal-square-root refinement
|
||||
formed `r*r` from a positive subnormal squared length. A finite correction
|
||||
of `1e-21` produces squared bits `0x000002CA` and a finite inverse root near
|
||||
`9.997e20`, whose square overflows to infinity. The two refinements then
|
||||
produce NaN. A focused pre-fix regression reproduced an all-NaN limited
|
||||
velocity from otherwise finite vectors. This establishes the numerical
|
||||
failure independently of the collision system; it is not a replay of all
|
||||
4059 input ticks.
|
||||
|
||||
The fix normalizes only positive subnormal squared lengths by the exact
|
||||
power of two `2^24`, retains both refinement stages, and rescales the inverse
|
||||
root by `2^12`. It preserves the ordinary-input path, movement limits and
|
||||
invalid-input propagation. No global floating-point mode changes, collision
|
||||
bypass, arbitrary epsilon, or replacement player state are involved. This
|
||||
is a stability correction for the existing portable PC arithmetic, not a
|
||||
claim of bit-exact Xenon instruction emulation.
|
||||
|
||||
Regressions cover the failing finite vectors, powers-of-two across the
|
||||
subnormal/normal boundary, limiting tiny corrections, steady walking updates,
|
||||
and the complete ground-controller publication (including position, lean and
|
||||
cadence). Zero and NaN checks ensure invalid inputs are not silently repaired.
|
||||
The report, matching stderr and arithmetic evidence are retained privately
|
||||
with the test builds. Gameplay validation remains with the user.
|
||||
|
||||
Validation: the finite-delta regression failed before the arithmetic change
|
||||
with `[NaN, NaN, NaN, NaN]`. Afterward all 11 movement-velocity tests passed,
|
||||
followed by all 202 offboard library tests in the release MSVC configuration
|
||||
(`cargo test --release --locked --target x86_64-pc-windows-msvc -p skate-core
|
||||
--lib player::offboard::`). No game or renderer was launched.
|
||||
|
||||
## FingerFlipOut (2026-09-09)
|
||||
|
||||
Observed user failure: clean revision `1a6e95b`, build timestamp
|
||||
|
||||
Reference in new issue
Block a user