From c56469a366193b268aab4e73c40db0cf78f86381 Mon Sep 17 00:00:00 2001 From: yohawing Date: Tue, 21 Jul 2026 16:55:50 +0900 Subject: [PATCH] fix(usdc): decode integer-compressed float arrays `read_floats` treated the `i` (compressed-integers) branch of a float array's value rep as plain LZ4-compressed `i32`s and cast them straight to `f32`. Pixar's crateFile.cpp `_ReadCompressedInts` (see https://github.com/PixarAnimationStudios/OpenUSD/blob/release/pxr/usd/usd/crateFile.cpp) additionally runs the decompressed buffer through `Usd_IntegerCompression` decoding before returning the integers, because the `i` code is USD's compact encoding for arrays whose values happen to all be integral, not a marker for "raw i32 dump". Skipping that decode step silently produces garbage floats. This only reproduces when every element of a `float[]`/`double[]` array is an integral value (e.g. joint weights that are all `0.0`/`1.0`), which is exactly the case where the crate writer picks the `i` array encoding. A 16-element all-integral `float[]` written by `usdcat` decoded to values such as `269505630.0` and `-16646399.0` instead of the original `0.0`/`1.0` before this fix. Fix by routing the `i` branch through `read_encoded_ints`, the same Usd_IntegerCompression-aware decoder already used elsewhere in this file for token/path/spec integer sections, instead of the raw `read_compressed` LZ4-only path. Adds a regression fixture (`fixtures/integer_compressed_floats.usdc`, generated with upstream `usdcat` from the paired `.usda`) plus two tests: one exercising the fixture through the public field-reading API and one asserting the fixture actually triggers the `i` array-value-rep code path, so a future refactor can't silently drop coverage of this branch. --- fixtures/integer_compressed_floats.usda | 13 ++++++++++ fixtures/integer_compressed_floats.usdc | Bin 0 -> 745 bytes src/usdc/mod.rs | 18 +++++++++++++ src/usdc/reader.rs | 32 ++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 fixtures/integer_compressed_floats.usda create mode 100644 fixtures/integer_compressed_floats.usdc diff --git a/fixtures/integer_compressed_floats.usda b/fixtures/integer_compressed_floats.usda new file mode 100644 index 0000000..f36d73a --- /dev/null +++ b/fixtures/integer_compressed_floats.usda @@ -0,0 +1,13 @@ +#usda 1.0 + +# Regression fixture for USDC's `i` (integer-compressed float array) encoding. +# Generated with OpenUSD `usdcat -o integer_compressed_floats.usdc`. +def Scope "IntegerCompressedFloats" +{ + float[] weights = [ + 1.0, 0.0, 1.0, 0.0, + 0.0, 1.0, 0.0, 1.0, + 1.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 1.0, + ] +} diff --git a/fixtures/integer_compressed_floats.usdc b/fixtures/integer_compressed_floats.usdc new file mode 100644 index 0000000000000000000000000000000000000000..adf5e052629ffedda2b9f7997710540bbcd8dd3f GIT binary patch literal 745 zcmbtR%TB^j5S=2GDjK_}i92N{Mxw-(x+y6|hz1EQk;Ir-;Q}{lY10-B@dNw-hnLC|nw3~&NR#rfO`N^eX{O_B?0uiz}6UHL)RKftJ z^BY^bC~^TltEi0;?^*8q;c+1kwV_Y?#8qs|af#=VZpE|>-vh^`mNKAb*Cj&$&mkS! zqr?TRV><-=u|w*HMc|Im{$MY>H)LI(5)V2f&$lh;RbeVzUcs&5Qsai2)E`5a^o)_| zvj*6hV1+T0xP9)6=MEUR)eiqz%=2J`%#O?pP|V6*0n!^#WOQJfOabdZVOQnHEvc RsFzUJ8mjVJe?$EU^&btnT*LqX literal 0 HcmV?d00001 diff --git a/src/usdc/mod.rs b/src/usdc/mod.rs index 2582c1d..5c04017 100644 --- a/src/usdc/mod.rs +++ b/src/usdc/mod.rs @@ -772,6 +772,24 @@ mod tests { Ok(()) } + #[test] + fn test_read_integer_compressed_float_array() -> Result<()> { + // OpenUSD serialises an all-integral float array with the `i` code: + // its LZ4 payload still needs Usd_IntegerCompression decoding. + let data = read_file("fixtures/integer_compressed_floats.usdc")?; + let weights = data + .get_field(&sdf::path("/IntegerCompressedFloats.weights")?, "default")? + .into_owned() + .try_as_float_vec() + .unwrap(); + + assert_eq!( + weights, + vec![1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0,] + ); + Ok(()) + } + #[test] fn test_read_halfs() -> Result<()> { let data = read_file("fixtures/floats.usdc")?; diff --git a/src/usdc/reader.rs b/src/usdc/reader.rs index 38a608b..42d4e2b 100644 --- a/src/usdc/reader.rs +++ b/src/usdc/reader.rs @@ -662,9 +662,13 @@ impl CrateFile { let code = self.reader.read_pod::()?; match code { - // Compressed integers + // Compressed integers. Pixar's `_ReadCompressedInts` runs the + // LZ4 block through `Usd_IntegerCompression` decoding after + // decompression, so the payload must be integer-decoded + // (`read_encoded_ints`), not reinterpreted as raw `i32`s + // straight out of LZ4. b'i' => { - let ints: Vec = self.read_compressed(count)?; + let ints: Vec = self.read_encoded_ints(count)?; ints.into_iter().map(|i| cast(i).unwrap()).collect() } // Lookup table and indexes @@ -1546,6 +1550,30 @@ mod tests { use super::*; use std::fs; + #[test] + fn integer_compressed_float_fixture_uses_i_encoding() -> Result<()> { + let mut file = CrateFile::open(fs::File::open("fixtures/integer_compressed_floats.usdc")?)?; + let value = file + .fields + .iter() + .find_map(|field| { + let ty = field.value_rep.ty().ok()?; + (file.tokens[field.token_index] == "default" + && ty == Type::Float + && field.value_rep.is_array() + && field.value_rep.is_compressed()) + .then_some(field.value_rep) + }) + .expect("fixture must contain one compressed float default value"); + + file.set_position(value.payload())?; + let (count, compressed) = file.unpack_array_len(value, ArrayKind::Floats)?; + assert_eq!(count, 16); + assert!(compressed); + assert_eq!(file.reader.read_pod::()?, b'i'); + Ok(()) + } + #[test] fn test_read_crate_struct() { let path = "./vendor/usd-wg-assets/full_assets/ElephantWithMonochord/SoC-ElephantWithMonochord.usdc";