use std::io::Read;
use flate2::read::GzDecoder;
use thiserror::Error;
use crate::splats::{RawSplat, SplatCoordinateConvention, Splats, sh_coefficients_per_splat};
const SPZ_MAGIC: u32 = 0x5053474e; const SH_C0: f32 = 0.282_094_77;
const MAX_DECOMPRESSED_BYTES: usize = 512 * 1024 * 1024;
pub const DEFAULT_MAX_SPLATS: u32 = 10_000_000;
const MIN_SPLAT_SCALE: f32 = 1e-6;
#[derive(Debug, Error)]
pub enum SpzError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("invalid SPZ file: bad magic")]
BadMagic,
#[error("unsupported SPZ version: {0}")]
UnsupportedVersion(u32),
#[error("unsupported SPZ SH degree: {0}")]
UnsupportedShDegree(u8),
#[error("invalid SPZ fractional bits: {0}")]
InvalidFractionalBits(u8),
#[error("invalid SPZ file: section size overflow")]
SizeOverflow,
#[error("truncated SPZ {section} section")]
TruncatedSection { section: &'static str },
#[error("invalid decoded SPZ {field} value")]
InvalidDecodedValue { field: &'static str },
#[error("SPZ decompressed payload exceeds {limit} bytes")]
DecompressedTooLarge { limit: usize },
#[error("SPZ splat count {0} exceeds supported maximum")]
TooManySplats(u32),
#[error("truncated SPZ data")]
Truncated,
}
#[derive(Debug, Clone, Copy)]
pub struct SpzHeader {
pub version: u32,
pub num_splats: u32,
pub sh_degree: u8,
pub fractional_bits: u8,
pub flags: u8,
pub anti_aliased: bool,
pub lod: bool,
}
pub fn parse_spz(data: &[u8]) -> Result<Splats, SpzError> {
parse_spz_with_limits(data, MAX_DECOMPRESSED_BYTES, DEFAULT_MAX_SPLATS)
}
pub fn parse_spz_with_max_splats(data: &[u8], max_splats: u32) -> Result<Splats, SpzError> {
parse_spz_with_limits(data, MAX_DECOMPRESSED_BYTES, max_splats)
}
fn parse_spz_with_limits(
data: &[u8],
max_decompressed_bytes: usize,
max_splats: u32,
) -> Result<Splats, SpzError> {
let mut decoder = GzDecoder::new(data);
let read_limit = max_decompressed_bytes.saturating_add(1) as u64;
let mut limited = decoder.by_ref().take(read_limit);
let mut all = Vec::with_capacity(data.len().saturating_mul(4).min(read_limit as usize));
limited.read_to_end(&mut all)?;
if all.len() > max_decompressed_bytes {
return Err(SpzError::DecompressedTooLarge {
limit: max_decompressed_bytes,
});
}
parse_spz_inflated_with_max_splats(&all, max_splats)
}
pub fn parse_spz_inflated(buf: &[u8]) -> Result<Splats, SpzError> {
parse_spz_inflated_with_max_splats(buf, DEFAULT_MAX_SPLATS)
}
pub fn parse_spz_inflated_with_max_splats(buf: &[u8], max_splats: u32) -> Result<Splats, SpzError> {
if buf.len() < 16 {
return Err(SpzError::Truncated);
}
let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
if magic != SPZ_MAGIC {
return Err(SpzError::BadMagic);
}
let version = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
if !(1..=3).contains(&version) {
return Err(SpzError::UnsupportedVersion(version));
}
let num_splats_u32 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
if num_splats_u32 > max_splats {
return Err(SpzError::TooManySplats(num_splats_u32));
}
let num_splats = num_splats_u32 as usize;
let sh_degree = buf[12];
if sh_degree > 3 {
return Err(SpzError::UnsupportedShDegree(sh_degree));
}
let fractional_bits = buf[13];
if version != 1 && fractional_bits >= u32::BITS as u8 {
return Err(SpzError::InvalidFractionalBits(fractional_bits));
}
let flags = buf[14];
let anti_aliased = (flags & 0x01) != 0;
let lod = (flags & 0x80) != 0;
let sh_stride = sh_coefficients_per_splat(sh_degree as u32);
let center_size = if version == 1 { 6 } else { 9 };
let quaternion_size = if version == 3 { 4 } else { 3 };
let mut required = 16usize;
required = checked_add_section(required, num_splats, center_size, "centers", buf.len())?;
required = checked_add_section(required, num_splats, 1, "alphas", buf.len())?;
required = checked_add_section(required, num_splats, 3, "colors", buf.len())?;
required = checked_add_section(required, num_splats, 3, "scales", buf.len())?;
required = checked_add_section(
required,
num_splats,
quaternion_size,
"quaternions",
buf.len(),
)?;
required = checked_add_section(required, num_splats, sh_stride, "sh", buf.len())?;
if lod {
required = checked_add_section(required, num_splats, 2, "lod_child_counts", buf.len())?;
let _ = checked_add_section(required, num_splats, 4, "lod_child_starts", buf.len())?;
}
let _header = SpzHeader {
version,
num_splats: num_splats_u32,
sh_degree,
fractional_bits,
flags,
anti_aliased,
lod,
};
let mut cursor = 16usize;
let mut splats = Vec::with_capacity(num_splats);
splats.resize(num_splats, RawSplat::default());
if version == 1 {
let need = num_splats * 6;
let bytes = chunk(buf, &mut cursor, need, "centers")?;
for (splat, bytes) in splats.iter_mut().zip(bytes.chunks_exact(6)) {
let x = half::f16::from_le_bytes([bytes[0], bytes[1]]).to_f32();
let y = half::f16::from_le_bytes([bytes[2], bytes[3]]).to_f32();
let z = half::f16::from_le_bytes([bytes[4], bytes[5]]).to_f32();
splat.center = [x, y, z];
}
} else {
let fixed = (1u32 << fractional_bits) as f32;
let need = num_splats * 9;
let bytes = chunk(buf, &mut cursor, need, "centers")?;
for (splat, bytes) in splats.iter_mut().zip(bytes.chunks_exact(9)) {
let x = read_i24_le(&bytes[0..3]) as f32 / fixed;
let y = read_i24_le(&bytes[3..6]) as f32 / fixed;
let z = read_i24_le(&bytes[6..9]) as f32 / fixed;
splat.center = [x, y, z];
}
}
{
let need = num_splats;
let bytes = chunk(buf, &mut cursor, need, "alphas")?;
for (splat, alpha) in splats.iter_mut().zip(bytes.iter()) {
splat.alpha = *alpha as f32 / 255.0;
}
}
{
let need = num_splats * 3;
let bytes = chunk(buf, &mut cursor, need, "colors")?;
let scale = SH_C0 / 0.15;
for (splat, bytes) in splats.iter_mut().zip(bytes.chunks_exact(3)) {
let r = (bytes[0] as f32 / 255.0 - 0.5) * scale + 0.5;
let g = (bytes[1] as f32 / 255.0 - 0.5) * scale + 0.5;
let b = (bytes[2] as f32 / 255.0 - 0.5) * scale + 0.5;
splat.color = [r, g, b];
}
}
{
let need = num_splats * 3;
let bytes = chunk(buf, &mut cursor, need, "scales")?;
for (splat, bytes) in splats.iter_mut().zip(bytes.chunks_exact(3)) {
let sx = ((bytes[0] as f32) / 16.0 - 10.0).exp();
let sy = ((bytes[1] as f32) / 16.0 - 10.0).exp();
let sz = ((bytes[2] as f32) / 16.0 - 10.0).exp();
splat.scale = [sx, sy, sz];
}
}
if version == 3 {
let max_v = 1.0 / 2f32.sqrt();
let need = num_splats * 4;
let bytes = chunk(buf, &mut cursor, need, "quaternions")?;
for (splat, bytes) in splats.iter_mut().zip(bytes.chunks_exact(4)) {
let combined = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let largest = (combined >> 30) as usize;
let value_mask = (1u32 << 9) - 1;
let mut quat = [0f32; 4];
let mut sum_sq = 0f32;
let mut remaining = combined;
for j in (0..4).rev() {
if j != largest {
let value = remaining & value_mask;
let sign = (remaining >> 9) & 1;
remaining >>= 10;
let mut v = max_v * (value as f32 / value_mask as f32);
if sign != 0 {
v = -v;
}
quat[j] = v;
sum_sq += v * v;
}
}
quat[largest] = (1.0 - sum_sq).max(0.0).sqrt();
splat.quat = quat;
}
} else {
let need = num_splats * 3;
let bytes = chunk(buf, &mut cursor, need, "quaternions")?;
for (splat, bytes) in splats.iter_mut().zip(bytes.chunks_exact(3)) {
let qx = bytes[0] as f32 / 127.5 - 1.0;
let qy = bytes[1] as f32 / 127.5 - 1.0;
let qz = bytes[2] as f32 / 127.5 - 1.0;
let qw = (1.0 - qx * qx - qy * qy - qz * qz).max(0.0).sqrt();
splat.quat = [qx, qy, qz, qw];
}
}
let sh_coefficients = if sh_stride == 0 {
Vec::new()
} else {
let bytes = chunk(buf, &mut cursor, num_splats * sh_stride, "sh")?;
bytes
.iter()
.map(|byte| (*byte as i16 - 128) as i8)
.collect()
};
let (lod_child_counts, lod_child_starts) = if lod {
let count_bytes = chunk(buf, &mut cursor, num_splats * 2, "lod_child_counts")?;
let mut child_counts = Vec::with_capacity(num_splats);
for i in 0..num_splats {
let off = i * 2;
child_counts.push(u16::from_le_bytes([count_bytes[off], count_bytes[off + 1]]));
}
let start_bytes = chunk(buf, &mut cursor, num_splats * 4, "lod_child_starts")?;
let mut child_starts = Vec::with_capacity(num_splats);
for i in 0..num_splats {
let off = i * 4;
child_starts.push(u32::from_le_bytes([
start_bytes[off],
start_bytes[off + 1],
start_bytes[off + 2],
start_bytes[off + 3],
]));
}
(child_counts, child_starts)
} else {
(Vec::new(), Vec::new())
};
for splat in &mut splats {
sanitize_splat(splat)?;
}
Ok(Splats {
splats,
anti_aliased,
sh_degree: sh_degree as u32,
sh_coefficients,
header_version: version,
lod,
lod_child_counts,
lod_child_starts,
coordinate_convention: SplatCoordinateConvention::BevyYUp,
})
}
fn chunk<'a>(
buf: &'a [u8],
cursor: &mut usize,
n: usize,
section: &'static str,
) -> Result<&'a [u8], SpzError> {
let end = *cursor + n;
if end > buf.len() {
return Err(SpzError::TruncatedSection { section });
}
let slice = &buf[*cursor..end];
*cursor = end;
Ok(slice)
}
fn checked_add_section(
total: usize,
num_splats: usize,
bytes_per_splat: usize,
section: &'static str,
buf_len: usize,
) -> Result<usize, SpzError> {
let section_len = num_splats
.checked_mul(bytes_per_splat)
.ok_or(SpzError::SizeOverflow)?;
let end = total
.checked_add(section_len)
.ok_or(SpzError::SizeOverflow)?;
if end > buf_len {
return Err(SpzError::TruncatedSection { section });
}
Ok(end)
}
fn sanitize_splat(splat: &mut RawSplat) -> Result<(), SpzError> {
if !splat.center.iter().all(|v| v.is_finite()) {
return Err(SpzError::InvalidDecodedValue { field: "center" });
}
if !splat.alpha.is_finite() {
return Err(SpzError::InvalidDecodedValue { field: "alpha" });
}
if !splat.color.iter().all(|v| v.is_finite()) {
return Err(SpzError::InvalidDecodedValue { field: "color" });
}
if !splat.scale.iter().all(|v| v.is_finite()) {
return Err(SpzError::InvalidDecodedValue { field: "scale" });
}
for scale in &mut splat.scale {
*scale = scale.max(MIN_SPLAT_SCALE);
}
if !splat.quat.iter().all(|v| v.is_finite()) {
return Err(SpzError::InvalidDecodedValue {
field: "quaternion",
});
}
let len2 = splat.quat.iter().map(|v| v * v).sum::<f32>();
if len2 <= 1e-12 {
splat.quat = [0.0, 0.0, 0.0, 1.0];
} else {
let inv_len = len2.sqrt().recip();
for value in &mut splat.quat {
*value *= inv_len;
}
}
Ok(())
}
fn read_i24_le(b: &[u8]) -> i32 {
let raw = (b[0] as u32) | ((b[1] as u32) << 8) | ((b[2] as u32) << 16);
let mut v = raw as i32;
if (v & 0x00800000) != 0 {
v |= 0xff000000u32 as i32;
}
v
}
#[cfg(test)]
mod tests {
use std::io::Write;
use flate2::Compression;
use flate2::write::GzEncoder;
use super::*;
fn one_splat_v3(sh_degree: u8, sh_bytes: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&SPZ_MAGIC.to_le_bytes());
buf.extend_from_slice(&3u32.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.push(sh_degree);
buf.push(12); buf.push(0); buf.push(0);
buf.extend_from_slice(&[0; 9]); buf.push(255); buf.extend_from_slice(&[128; 3]); buf.extend_from_slice(&[160; 3]); buf.extend_from_slice(&[0; 4]); buf.extend_from_slice(sh_bytes);
buf
}
fn one_splat_v1(center_bytes: [u8; 6], scale_bytes: [u8; 3], quat_bytes: [u8; 3]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&SPZ_MAGIC.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.push(0); buf.push(0); buf.push(0); buf.push(0);
buf.extend_from_slice(¢er_bytes);
buf.push(255); buf.extend_from_slice(&[128; 3]); buf.extend_from_slice(&scale_bytes);
buf.extend_from_slice(&quat_bytes);
buf
}
fn one_splat_v2(center: [i32; 3]) -> Vec<u8> {
fn push_i24(buf: &mut Vec<u8>, value: i32) {
let raw = value as u32;
buf.push((raw & 0xff) as u8);
buf.push(((raw >> 8) & 0xff) as u8);
buf.push(((raw >> 16) & 0xff) as u8);
}
let mut buf = Vec::new();
buf.extend_from_slice(&SPZ_MAGIC.to_le_bytes());
buf.extend_from_slice(&2u32.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.push(0); buf.push(8); buf.push(0); buf.push(0);
for value in center {
push_i24(&mut buf, value);
}
buf.push(255); buf.extend_from_slice(&[128; 3]); buf.extend_from_slice(&[160; 3]); buf.extend_from_slice(&[128; 3]); buf
}
fn two_splat_v3_lod() -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&SPZ_MAGIC.to_le_bytes());
buf.extend_from_slice(&3u32.to_le_bytes());
buf.extend_from_slice(&2u32.to_le_bytes());
buf.push(0); buf.push(12); buf.push(0x80); buf.push(0);
buf.extend_from_slice(&[0; 18]); buf.extend_from_slice(&[255; 2]); buf.extend_from_slice(&[128; 6]); buf.extend_from_slice(&[160; 6]); buf.extend_from_slice(&[0; 8]); buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&0u16.to_le_bytes());
buf.extend_from_slice(&5u32.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf
}
#[test]
fn stores_degree_1_2_3_sh_payloads() {
for degree in 1..=3 {
let stride = sh_coefficients_per_splat(degree as u32);
let sh_bytes = (0..stride)
.map(|i| ((i * 37 + 11) & 0xff) as u8)
.collect::<Vec<_>>();
let splats = parse_spz_inflated(&one_splat_v3(degree, &sh_bytes)).unwrap();
assert_eq!(splats.sh_degree, degree as u32);
assert_eq!(splats.sh_coefficients.len(), stride);
for (actual, byte) in splats.sh_coefficients.iter().zip(sh_bytes) {
assert_eq!(*actual, (byte as i16 - 128) as i8);
}
}
}
#[test]
fn degree_zero_has_no_sh_payload() {
let splats = parse_spz_inflated(&one_splat_v3(0, &[])).unwrap();
assert_eq!(splats.sh_degree, 0);
assert!(splats.sh_coefficients.is_empty());
}
#[test]
fn decodes_v1_half_centers() {
let x = half::f16::from_f32(1.0).to_le_bytes();
let y = half::f16::from_f32(-2.0).to_le_bytes();
let z = half::f16::from_f32(0.5).to_le_bytes();
let splats = parse_spz_inflated(&one_splat_v1(
[x[0], x[1], y[0], y[1], z[0], z[1]],
[160; 3],
[128; 3],
))
.unwrap();
assert_eq!(splats.splats[0].center, [1.0, -2.0, 0.5]);
}
#[test]
fn decodes_v2_fixed_point_centers() {
let splats = parse_spz_inflated(&one_splat_v2([256, -512, 128])).unwrap();
assert_eq!(splats.splats[0].center, [1.0, -2.0, 0.5]);
}
#[test]
fn decodes_v3_smallest_three_quaternion() {
let splats = parse_spz_inflated(&one_splat_v3(0, &[])).unwrap();
assert_eq!(splats.splats[0].quat, [1.0, 0.0, 0.0, 0.0]);
}
#[test]
fn stores_anti_aliased_flag() {
let mut buf = one_splat_v3(0, &[]);
buf[14] = 0x01;
let splats = parse_spz_inflated(&buf).unwrap();
assert!(splats.anti_aliased);
}
#[test]
fn rejects_bad_magic_before_allocation() {
let mut buf = one_splat_v3(0, &[]);
buf[0] = 0;
assert!(matches!(parse_spz_inflated(&buf), Err(SpzError::BadMagic)));
}
#[test]
fn rejects_unsupported_version_before_allocation() {
let mut buf = one_splat_v3(0, &[]);
buf[4..8].copy_from_slice(&4u32.to_le_bytes());
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::UnsupportedVersion(4))
));
}
#[test]
fn rejects_unsupported_sh_degree() {
let result = parse_spz_inflated(&one_splat_v3(4, &[]));
assert!(matches!(result, Err(SpzError::UnsupportedShDegree(4))));
}
#[test]
fn rejects_fractional_bits_that_would_overflow_shift() {
let mut buf = one_splat_v3(0, &[]);
buf[13] = 32;
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::InvalidFractionalBits(32))
));
}
#[test]
fn rejects_too_many_splats_before_layout_or_allocation() {
let mut buf = one_splat_v3(0, &[]);
buf[8..12].copy_from_slice(&(DEFAULT_MAX_SPLATS + 1).to_le_bytes());
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::TooManySplats(count)) if count == DEFAULT_MAX_SPLATS + 1
));
}
#[test]
fn custom_max_splats_rejects_smaller_scene_limits() {
let buf = one_splat_v3(0, &[]);
assert!(matches!(
parse_spz_inflated_with_max_splats(&buf, 0),
Err(SpzError::TooManySplats(1))
));
}
#[test]
fn reports_truncated_section_name() {
let mut buf = one_splat_v3(0, &[]);
let colors_start = 16 + 9 + 1;
buf.truncate(colors_start + 2);
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::TruncatedSection { section: "colors" })
));
}
#[test]
fn reports_truncation_for_each_payload_section() {
let sh_stride = sh_coefficients_per_splat(1);
let full = one_splat_v3(1, &vec![128; sh_stride]);
let cases = [
(16 + 8, "centers"),
(16 + 9, "alphas"),
(16 + 9 + 1 + 2, "colors"),
(16 + 9 + 1 + 3 + 2, "scales"),
(16 + 9 + 1 + 3 + 3 + 3, "quaternions"),
(16 + 9 + 1 + 3 + 3 + 4 + sh_stride - 1, "sh"),
];
for (len, section) in cases {
let mut truncated = full.clone();
truncated.truncate(len);
assert!(
matches!(
parse_spz_inflated(&truncated),
Err(SpzError::TruncatedSection { section: actual }) if actual == section
),
"expected {section} truncation"
);
}
let mut lod_counts = two_splat_v3_lod();
lod_counts.truncate(16 + 18 + 2 + 6 + 6 + 8 + 1);
assert!(matches!(
parse_spz_inflated(&lod_counts),
Err(SpzError::TruncatedSection {
section: "lod_child_counts"
})
));
let mut lod_starts = two_splat_v3_lod();
lod_starts.pop();
assert!(matches!(
parse_spz_inflated(&lod_starts),
Err(SpzError::TruncatedSection {
section: "lod_child_starts"
})
));
}
#[test]
fn accounts_for_sh_payload_in_layout() {
let mut buf = one_splat_v3(1, &vec![128; sh_coefficients_per_splat(1)]);
buf.pop();
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::TruncatedSection { section: "sh" })
));
}
#[test]
fn accounts_for_lod_payload_in_layout() {
let mut buf = two_splat_v3_lod();
buf.pop();
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::TruncatedSection {
section: "lod_child_starts"
})
));
}
#[test]
fn allows_trailing_bytes_for_forward_compatibility() {
let mut buf = one_splat_v3(0, &[]);
buf.extend_from_slice(b"future-extension");
let splats = parse_spz_inflated(&buf).unwrap();
assert_eq!(splats.len(), 1);
}
#[test]
fn rejects_non_finite_decoded_centers() {
let nan = half::f16::NAN.to_le_bytes();
let zero = half::f16::from_f32(0.0).to_le_bytes();
let buf = one_splat_v1(
[nan[0], nan[1], zero[0], zero[1], zero[0], zero[1]],
[160; 3],
[128; 3],
);
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::InvalidDecodedValue { field: "center" })
));
}
#[test]
fn normalizes_decoded_quaternions() {
let zero = half::f16::from_f32(0.0).to_le_bytes();
let buf = one_splat_v1(
[zero[0], zero[1], zero[0], zero[1], zero[0], zero[1]],
[0; 3],
[255, 127, 127],
);
let splats = parse_spz_inflated(&buf).unwrap();
let splat = splats.splats[0];
let quat_len = splat.quat.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!((quat_len - 1.0).abs() < 1e-5);
}
#[test]
fn clamps_near_zero_scales_intentionally() {
let mut splat = RawSplat {
center: [0.0, 0.0, 0.0],
alpha: 1.0,
color: [0.5, 0.5, 0.5],
scale: [0.0, -1.0, 1e-8],
quat: [0.0, 0.0, 0.0, 0.0],
};
sanitize_splat(&mut splat).unwrap();
assert_eq!(splat.scale, [MIN_SPLAT_SCALE; 3]);
assert_eq!(splat.quat, [0.0, 0.0, 0.0, 1.0]);
}
#[test]
fn huge_claimed_splat_count_is_rejected_before_allocation() {
let mut buf = one_splat_v3(0, &[]);
buf[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
buf.truncate(16);
assert!(matches!(
parse_spz_inflated(&buf),
Err(SpzError::TooManySplats(u32::MAX))
));
}
#[test]
fn bounds_gzip_decompression() {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&one_splat_v3(0, &[])).unwrap();
let compressed = encoder.finish().unwrap();
assert!(matches!(
parse_spz_with_limits(&compressed, 8, DEFAULT_MAX_SPLATS),
Err(SpzError::DecompressedTooLarge { limit: 8 })
));
}
#[test]
fn stores_lod_child_counts_and_starts() {
let splats = parse_spz_inflated(&two_splat_v3_lod()).unwrap();
assert!(splats.lod);
assert_eq!(splats.lod_child_counts, vec![2, 0]);
assert_eq!(splats.lod_child_starts, vec![5, 0]);
}
}