From c7836d6c49e4d151cfeacb8db91f2189f4e63364 Mon Sep 17 00:00:00 2001 From: David Delgado Date: Thu, 20 Aug 2026 01:06:15 +1200 Subject: [PATCH] fix(serializing): correct two's-complement unwrap in quaternion decompression ScaleToUint encodes the smaller components as two's complement masked to BitsPerAxis bits, but ScaleToFloat unwrapped negatives by subtracting Maximum * 2 -- that is 2 * IntScale steps, not the IntMask + 1 steps the mask actually wrapped by. Every negative component decoded exactly two quantization steps high; positives were unaffected. Round-tripping 200k values through Quaternion32: before: worst error 0.00345941 on negatives, 0.00069195 on positives after: worst error 0.00069195 on both (== half a step, the optimum) Small magnitudes could even flip sign: -0.001 decoded as +0.00138. Quaternion64Compression.ScaleToFloat_H/_L carried the same off-by-two. The encoder is untouched, so this is wire-compatible in both directions; only the decode of already-negative components changes. --- .../Helping/Quaternion32Compression.cs | 8 +++----- .../Helping/Quaternion64Compression.cs | 16 ++++++---------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Assets/FishNet/Runtime/Serializing/Helping/Quaternion32Compression.cs b/Assets/FishNet/Runtime/Serializing/Helping/Quaternion32Compression.cs index 341ec9a0..86bbae7d 100644 --- a/Assets/FishNet/Runtime/Serializing/Helping/Quaternion32Compression.cs +++ b/Assets/FishNet/Runtime/Serializing/Helping/Quaternion32Compression.cs @@ -118,11 +118,9 @@ private static uint ScaleToUint(float v) private static float ScaleToFloat(uint v) { - float unscaled = v * Maximum / IntScale; - - if (unscaled > Maximum) - unscaled -= Maximum * 2; - return unscaled; + // Values above IntScale are two's-complement negatives produced by ScaleToUint's mask. + int signed = v > IntScale ? (int)v - (IntMask + 1) : (int)v; + return signed * Maximum / IntScale; } /// diff --git a/Assets/FishNet/Runtime/Serializing/Helping/Quaternion64Compression.cs b/Assets/FishNet/Runtime/Serializing/Helping/Quaternion64Compression.cs index c29a0cd4..70113f56 100644 --- a/Assets/FishNet/Runtime/Serializing/Helping/Quaternion64Compression.cs +++ b/Assets/FishNet/Runtime/Serializing/Helping/Quaternion64Compression.cs @@ -111,20 +111,16 @@ private static ulong ScaleToUint_L(float v) private static float ScaleToFloat_H(ulong v) { - float unscaled = v * Maximum / IntScale_H; - - if (unscaled > Maximum) - unscaled -= Maximum * 2; - return unscaled; + // Values above IntScale are two's-complement negatives produced by ScaleToUint's mask. + long signed = v > IntScale_H ? (long)v - (IntMask_H + 1) : (long)v; + return signed * Maximum / IntScale_H; } private static float ScaleToFloat_L(ulong v) { - float unscaled = v * Maximum / IntScale_L; - - if (unscaled > Maximum) - unscaled -= Maximum * 2; - return unscaled; + // Values above IntScale are two's-complement negatives produced by ScaleToUint's mask. + long signed = v > IntScale_L ? (long)v - (IntMask_L + 1) : (long)v; + return signed * Maximum / IntScale_L; } public static Quaternion Decompress(ulong compressed)