Skip to content

fixed 1.3.0 — the inverse carries its full width

Choose a tag to compare

@gafferongames gafferongames released this 24 Aug 09:06
· 20 commits to main since this release
db2e480

The 3x3 matrix inverse and solve are now correct at every magnitude, and say so when a result is not representable.

The bug. The inverse of a Q48.16 matrix is a ratio of two quantities that do not both fit in 128 bits — a cofactor reaches 126 bits, the determinant 190, an inverse numerator 158. The wide arm squeezed them in anyway, dropping 16 fraction bits from each cofactor and accumulating the determinant through an int64_t cast. Both steps were silent: the cast wrapped past 2^79 and the determinant overflowed shortly after, so past a certain size the function returned a value of arbitrary magnitude and arbitrary sign. For a physics consumer inverting an inertia tensor that means a positive-definite tensor inverting negative — a body that accelerates against its own torque.

The fix. fixed_int256.h carries the wide arm at a width that cannot overflow, built entirely on the fixUInt128 seam so it is native where __int128 exists and emulated where it does not. The exact arm's range test now names all nine cofactors rather than the three the determinant expands along. fixDivShifted saturates with the true sign instead of truncating to 64 bits. And fixSolve3's wide arm solves directly at 256 bits rather than inverting and multiplying, which kept one rounding where the old fallback took two through the coarsest value in the calculation.

The boundary is now documented and asserted. Q48.16 holds no value between zero and 1/65536, so the inverse of a matrix with entries past 65,536 is not a small number in this format — it is no number at all, and zero is the exact truncated answer rather than a failure. USAGE.md states where that line falls and what a zero result means; usage_test.c asserts it.

How it is held. inverse_envelope_test.c walks the magnitudes from cube side 1 to 500 — a factor of 10^12 in the cofactors — against an oracle that shares no arithmetic with the implementation, so it is an equality at every size rather than a tolerance. It was verified red against 1.2.1 (41 checks, including the sign checks). Its negative control is the historical bug restored verbatim rather than an invented perturbation, and it runs on the emulated 128-bit arm and under UBSan on both arms.

EXPECTED_GEOMETRY_HASH is re-captured deliberately; exactly two values move, both from the huge matrix in that suite, and every other value was checked bit-identical.

Also in this release since 1.2.1: 128-bit emulation making plain MSVC a supported compiler, the arbitrary-scale quantize and Q-format-crossing family, full test coverage of the geometric layer, and USAGE.md.