From 99785c81a14580e737106fbc197f211564f3e44f Mon Sep 17 00:00:00 2001 From: webertob Date: Sat, 22 Aug 2026 15:09:53 +0200 Subject: [PATCH] mdl: make Joint::clamp terminate for any finite q Joint::clamp normalises a wraparound joint by repeated subtraction: while (q(i) > this->max(i)) { q(i) -= range; } That has no termination guarantee. Once |q| is large enough that q - range == q in double precision the value stops changing and the loop never exits - on finite input, with an ordinary range of 2*pi. A range of zero does the same immediately. This is reachable from the iterative solvers. JacobianInverseKinematics::solve calls Metric::step, which calls Joint::step, which calls clamp, and a solve that diverges can produce a large iterate. The loop sits below solve()'s own iteration and duration checks, so neither bounds it: the calling thread spins with no error and no way out. In an application embedding the library, this presents as a freeze with one core pinned. Normalise with fmod instead. It is one step for any magnitude and cannot spin. Inputs fmod cannot express - a non-finite q, or a degenerate range - fall through to the existing clamp, so an infinity lands on the limit and a NaN stays a NaN for the caller to notice. The result is unchanged for inputs where the old loop terminated: both map q into [min, max] by whole multiples of the range. --- src/rl/mdl/Joint.cpp | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/rl/mdl/Joint.cpp b/src/rl/mdl/Joint.cpp index 2ac6c02e..5dbc1467 100644 --- a/src/rl/mdl/Joint.cpp +++ b/src/rl/mdl/Joint.cpp @@ -24,6 +24,7 @@ // POSSIBILITY OF SUCH DAMAGE. // +#include #include #include "Frame.h" @@ -67,19 +68,31 @@ namespace rl { for (::std::ptrdiff_t i = 0; i < q.size(); ++i) { - if (this->wraparound(i)) + ::rl::math::Real range = this->wraparound(i) + ? ::std::abs(this->max(i) - this->min(i)) + : ::rl::math::Real(0); + + // Wrapping by repeated subtraction has no termination guarantee. Once |q| is + // large enough that q - range == q in double precision the value stops moving, + // and the loop spins forever on finite, well formed input; a range of zero does + // the same immediately. An iterative solver can hand this a diverged iterate, + // and this sits below that solver's iteration and duration checks, so nothing + // above can bound it - the thread simply never comes back. + // + // fmod does the same normalisation in one step for any magnitude. Anything it + // cannot express - a non-finite q, or a degenerate range - falls through to a + // plain clamp: infinities land on the limit, and a NaN stays a NaN so the + // caller can still see something went wrong. + if (this->wraparound(i) && range > 0 && ::std::isfinite(q(i))) { - ::rl::math::Real range = ::std::abs(this->max(i) - this->min(i)); - - while (q(i) > this->max(i)) + ::rl::math::Real wrapped = ::std::fmod(q(i) - this->min(i), range); + + if (wrapped < 0) { - q(i) -= range; - } - - while (q(i) < this->min(i)) - { - q(i) += range; + wrapped += range; } + + q(i) = this->min(i) + wrapped; } else {