The only protection against a non-finite finite-difference derivative
here was assert(isfinite(fjac[ij])), which a release build (-DNDEBUG)
strips out entirely, and the two-sided derivative path had no check at
all. If the user function returns a degenerate residual for some
parameter value, wa[i]/fvec[i] can be non-finite, writing NaN/Inf
straight into fjac.
That NaN then poisons fnorm/ratio in mpfit()'s outer Levenberg-Marquardt
loop. Since any comparison against NaN is false, the iteration counter
(which only advances inside the ratio>=p0001 success branch) never
increments, so the maxiter check that's supposed to bound the loop never
fires. The result is an unbounded CPU spin instead of a clean failure
return -- in our case, confirmed via gdb on a real reproduction: ~465k
spin iterations/sec with zero progress, instead of returning MP_MAXITER.
Fix: sanitize fjac to 0 on a non-finite derivative in all three
non-debug/debug, one-sided/two-sided paths, so the column is treated as
having no measurable gradient -- the same fallback mp_qrfac already uses
for a zero-norm column.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
survive_plugins.unix.h: use .dylib extension and _NSGetExecutablePath()
on Apple. Also fixes a latent Linux bug where readlink() return value
was used as an array index without checking for error (-1).
CMakeLists.txt: find hidapi on Apple via pkg-config rather than
hardcoded Homebrew paths, which differ between Apple Silicon
(/opt/homebrew) and Intel (/usr/local).
cn_matrix.h: replace <malloc.h> with <stdlib.h> unconditionally, but
guard the re-inclusion of <malloc.h> with _WIN32. <malloc.h> does not
exist on macOS; <stdlib.h> provides malloc on all platforms. MSVC does
not declare alloca in <stdlib.h> — it lives in <malloc.h>.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SV_ERROR with SURVIVE_ERROR_HARWARE_FAULT crashes the process when an
unrecognised USB lightcap report ID is received. New tracker firmware
routinely emits report IDs that older libsurvive versions do not know;
crashing on an unrecognised ID makes libsurvive incompatible with any
firmware newer than it was built against.
The adjacent VIVE_REPORT_USB_TRACKER_LIGHTCAP_V1 case already uses
SV_INFO and ignores the packet. Apply the same treatment to the unknown
case: log a warning and continue so tracking on all known report types
is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A stalled USB endpoint must be cleared before resubmitting a transfer.
Without libusb_clear_halt(), libusb_submit_transfer() will return
LIBUSB_ERROR_PIPE (-9) on any endpoint left halted from a previous
session or failed transfer, causing the interface attachment to fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
An assert(isfinite(d[i])) crash here corrupts the on-disk libsurvive
config because the process dies mid-write. Corrupt optical angles
(e.g. bad FPGA timestamps during USB disturbances) can produce NaN or
Inf values that reach this function; crashing is strictly worse than
dropping one sample, which has negligible effect on the variance estimate.
Replace the assert with an early-return guard that logs to stderr and
leaves the accumulator unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MP_MAXITER (return code 5) means the solver exhausted its iteration
budget without converging. The current code treats this as success
(res <= 0 is false), so the unconverged — and potentially wrong —
lighthouse positions are written to disk via survive_recording and
the GSS result is accepted.
On the next launch those positions are loaded as the starting point
for calibration. If the unconverged solve is significantly wrong,
all subsequent tracking is corrupted and the only recovery is to
delete config.json.
Adding res == MP_MAXITER to the failure condition rejects unconverged
GSS solves the same way as explicit solver errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cap dt at 0.05s (5x the normal ~8ms IMU interval) before computing the
power series. State prediction still uses the real dt; only uncertainty
growth (Q matrix) is bounded. Without this cap, a gap of ~1s yields
t^7 = 1.0 which inflates Q enough to cause NaN/Inf in the filter on the
next update (observed as a quatrotateabout assertion failure on cold start
or blackout recovery). This matches standard practice for discretised
continuous-time process noise models where large gaps should widen
uncertainty to a finite maximum rather than to infinity.
Fixes: tracker freezes after lighthouse occlusion / blackout recovery
(collabora/libsurvive#346)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
linmath_max(1., linmath_min(-1, rtn)) always clamps the dot product to
1.0: linmath_min(-1, x) is always ≤ -1 for any x, then
linmath_max(1, ≤-1) = 1, so acos(|1.0|) = 0 for every input.
Fix: linmath_min(1., linmath_max(-1., rtn)) correctly clamps to [-1, 1].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quattomatrix33 outputs column-major (comment says "opengl major") but
quatfrommatrix33 reads row-major, and the 4x4 quattomatrix also outputs
row-major. This means a quat->matrix33->quat roundtrip produces the
conjugate (inverse rotation) instead of the original quaternion.
Fix by swapping the three off-diagonal pairs so quattomatrix33 outputs
row-major, consistent with the rest of the codebase.
The only runtime caller is barycentric_svd.c which passes the result to
cn_copy_in_row_major() — previously loading the transpose, now correct.
This doesn't affect tracking results because it only needs an arbitrary
valid rotation for control point initialization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
integration_variance[16] is stack-allocated but variance_tracker_calc()
may not write all elements when the tracker had zero observations
(counts == 0 or variance.size == 0). The uninitialized array is then
read by SV_VERBOSE via LINMATH_VEC7_EXPAND / LINMATH_VEC6_EXPAND.
Found by MSan when running test_replays.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`off64_t` is not a language defined type and is not available everywhere. Instead
the function should use `z_off_t` which is defined by zlib (and is most likely
`off_t`)