use crate::decode::{ByteReader, checked_product};
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
fn chunk_f32(chunk: &[u8], at: usize) -> f32 {
f32::from_le_bytes([chunk[at], chunk[at + 1], chunk[at + 2], chunk[at + 3]])
}
fn chunk_u32(chunk: &[u8], at: usize) -> u32 {
u32::from_le_bytes([chunk[at], chunk[at + 1], chunk[at + 2], chunk[at + 3]])
}
fn chunk_u16(chunk: &[u8], at: usize) -> u16 {
u16::from_le_bytes([chunk[at], chunk[at + 1]])
}
fn read_name(cur: &mut ByteReader<'_>, len: usize, what: &str) -> Result<String, String> {
core::str::from_utf8(cur.take(len)?)
.map_err(|e| format!("{what} is not valid utf-8: {e}"))
.map(str::to_string)
}
fn read_indices(cur: &mut ByteReader<'_>, n: usize, what: &str) -> Result<Vec<u16>, String> {
let block = cur.take(checked_product(what, &[n, 2])?)?;
Ok(block.chunks_exact(2).map(|c| chunk_u16(c, 0)).collect())
}
#[derive(Copy, Clone, Debug, bytemuck::NoUninit)]
#[repr(C)]
pub struct Vertex {
pub pos: [f32; 3],
pub normal: [f32; 3],
pub tangent: [f32; 3],
pub color: [f32; 3],
pub uv: [f32; 2],
}
type VertTuple = ([f32; 3], [f32; 3], [f32; 3], [f32; 3], [f32; 2]);
type LodAlternates = Vec<(f32, Vec<u16>)>;
type DeserialisedStatic = (Vec<Vertex>, Vec<u16>, LodAlternates);
type DeserialisedSkinned = (Vec<SkinnedVertex>, Vec<u16>, Vec<PayloadJoint>);
#[derive(Clone, Debug, Default)]
pub struct SkinnedPayload {
pub vertices: Vec<SkinnedVertex>,
pub indices: Vec<u16>,
pub joints: Vec<PayloadJoint>,
pub morphs: PayloadMorphs,
pub lods: LodAlternates,
}
pub fn serialise(vertices: &[VertTuple], indices: &[u16]) -> Vec<u8> {
let mut buf = Vec::with_capacity(4 + vertices.len() * 56 + 4 + indices.len() * 2);
buf.extend_from_slice(&(vertices.len() as u32).to_le_bytes());
for (pos, normal, tangent, color, uv) in vertices {
for x in pos
.iter()
.chain(normal.iter())
.chain(tangent.iter())
.chain(color.iter())
.chain(uv.iter())
{
buf.extend_from_slice(&x.to_le_bytes());
}
}
buf.extend_from_slice(&(indices.len() as u32).to_le_bytes());
for i in indices {
buf.extend_from_slice(&i.to_le_bytes());
}
buf
}
const LODS_MAGIC: &[u8; 4] = b"LODS";
pub fn serialise_with_lods(
vertices: &[VertTuple],
indices: &[u16],
lod_alternates: &[(f32, Vec<u16>)],
) -> Vec<u8> {
let mut buf = serialise(vertices, indices);
if lod_alternates.is_empty() {
return buf;
}
buf.extend_from_slice(LODS_MAGIC);
buf.extend_from_slice(&(lod_alternates.len() as u32).to_le_bytes());
for (distance, idx) in lod_alternates {
buf.extend_from_slice(&distance.to_le_bytes());
buf.extend_from_slice(&(idx.len() as u32).to_le_bytes());
for i in idx {
buf.extend_from_slice(&i.to_le_bytes());
}
}
buf
}
const HFLD_MAGIC: &[u8; 4] = b"HFLD";
pub struct HeightfieldGrid {
pub rows: usize,
pub cols: usize,
pub heights: Vec<f32>,
}
pub fn serialise_heightfield_trailer(rows: usize, cols: usize, heights: &[f32]) -> Vec<u8> {
let mut buf = Vec::with_capacity(4 + 4 + 4 + heights.len() * 4);
buf.extend_from_slice(HFLD_MAGIC);
buf.extend_from_slice(&(rows as u32).to_le_bytes());
buf.extend_from_slice(&(cols as u32).to_le_bytes());
for h in heights {
buf.extend_from_slice(&h.to_le_bytes());
}
buf
}
pub fn deserialise_heightfield(bytes: &[u8]) -> Result<Option<HeightfieldGrid>, String> {
let mut cur = ByteReader::new(bytes, "mesh payload");
let vertex_count = cur.u32()? as usize;
cur.skip(checked_product("vertices", &[vertex_count, 56])?)?;
let index_count = cur.u32()? as usize;
cur.skip(checked_product("indices", &[index_count, 2])?)?;
if cur.peek(LODS_MAGIC) {
cur.skip(4)?;
let alt_count = cur.u32()? as usize;
for _ in 0..alt_count {
cur.skip(4)?; let n = cur.u32()? as usize;
cur.skip(checked_product("lod indices", &[n, 2])?)?;
}
}
if !cur.peek(HFLD_MAGIC) {
return Ok(None);
}
cur.skip(4)?;
let rows = cur.u32()? as usize;
let cols = cur.u32()? as usize;
let count = checked_product("heightfield grid", &[rows, cols])?;
let block = cur
.take(checked_product("heightfield grid", &[count, 4])?)
.map_err(|_| format!("heightfield trailer too short for {rows} x {cols} grid"))?;
let heights = block.chunks_exact(4).map(|h| chunk_f32(h, 0)).collect();
Ok(Some(HeightfieldGrid {
rows,
cols,
heights,
}))
}
#[derive(Copy, Clone, Debug, PartialEq, bytemuck::NoUninit)]
#[repr(C)]
pub struct SkinnedVertex {
pub pos: [f32; 3],
pub normal: [f32; 3],
pub tangent: [f32; 3],
pub color: [f32; 3],
pub uv: [f32; 2],
pub joints: [u16; 4],
pub weights: [f32; 4],
}
const SKINNED_MAGIC: &[u8; 4] = b"SKMV";
const MORPH_MAGIC: &[u8; 4] = b"MRPS";
pub use super::morph_targets::{MORPH_DELTA_EPSILON, MorphDelta, MorphEntry, PayloadMorphs};
#[derive(Clone, Debug, PartialEq)]
pub struct PayloadJoint {
pub name: String,
pub parent: i32,
pub translation: [f32; 3],
pub rotation_deg: [f32; 3],
pub scale: [f32; 3],
}
#[cfg(test)]
pub(crate) fn serialise_skinned(
vertices: &[SkinnedVertex],
indices: &[u16],
joints: &[PayloadJoint],
) -> Vec<u8> {
serialise_skinned_with_lods(vertices, indices, joints, &PayloadMorphs::default(), &[])
}
pub fn serialise_skinned_with_lods(
vertices: &[SkinnedVertex],
indices: &[u16],
joints: &[PayloadJoint],
morphs: &PayloadMorphs,
lod_alternates: &[(f32, Vec<u16>)],
) -> Vec<u8> {
let mut buf = Vec::with_capacity(4 + 4 + vertices.len() * 80 + 4 + indices.len() * 2 + 4);
buf.extend_from_slice(SKINNED_MAGIC);
buf.extend_from_slice(&(vertices.len() as u32).to_le_bytes());
for v in vertices {
for f in v
.pos
.iter()
.chain(v.normal.iter())
.chain(v.tangent.iter())
.chain(v.color.iter())
.chain(v.uv.iter())
{
buf.extend_from_slice(&f.to_le_bytes());
}
for j in v.joints {
buf.extend_from_slice(&j.to_le_bytes());
}
for w in v.weights {
buf.extend_from_slice(&w.to_le_bytes());
}
}
buf.extend_from_slice(&(indices.len() as u32).to_le_bytes());
for i in indices {
buf.extend_from_slice(&i.to_le_bytes());
}
buf.extend_from_slice(&(joints.len() as u32).to_le_bytes());
for j in joints {
let name_bytes = j.name.as_bytes();
buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
buf.extend_from_slice(name_bytes);
buf.extend_from_slice(&j.parent.to_le_bytes());
for x in j
.translation
.iter()
.chain(j.rotation_deg.iter())
.chain(j.scale.iter())
{
buf.extend_from_slice(&x.to_le_bytes());
}
}
if !morphs.is_empty() {
buf.extend_from_slice(MORPH_MAGIC);
buf.extend_from_slice(&(morphs.names.len() as u32).to_le_bytes());
for name in &morphs.names {
let name_bytes = name.as_bytes();
buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
buf.extend_from_slice(name_bytes);
}
buf.extend_from_slice(&(morphs.entries.len() as u32).to_le_bytes());
for o in &morphs.offsets {
buf.extend_from_slice(&o.to_le_bytes());
}
for e in &morphs.entries {
buf.extend_from_slice(&e.target.to_le_bytes());
for x in e.position.iter().chain(e.normal.iter()) {
buf.extend_from_slice(&x.to_le_bytes());
}
}
}
if !lod_alternates.is_empty() {
buf.extend_from_slice(LODS_MAGIC);
buf.extend_from_slice(&(lod_alternates.len() as u32).to_le_bytes());
for (distance, idx) in lod_alternates {
buf.extend_from_slice(&distance.to_le_bytes());
buf.extend_from_slice(&(idx.len() as u32).to_le_bytes());
for i in idx {
buf.extend_from_slice(&i.to_le_bytes());
}
}
}
buf
}
pub fn deserialise_skinned(bytes: &[u8]) -> Result<DeserialisedSkinned, String> {
let p = deserialise_skinned_with_lods(bytes)?;
Ok((p.vertices, p.indices, p.joints))
}
pub fn deserialise_skinned_with_lods(bytes: &[u8]) -> Result<SkinnedPayload, String> {
if bytes.len() < 8 || &bytes[0..4] != SKINNED_MAGIC {
return Err("skinned mesh payload missing SKMV magic header".to_string());
}
let mut cur = ByteReader::new(bytes, "skinned mesh payload");
cur.skip(4)?;
let vertex_count = cur.u32()? as usize;
let vertices = read_skinned_vertices(&mut cur, vertex_count)?;
let index_count = cur.u32()? as usize;
let indices = read_indices(&mut cur, index_count, "indices")?;
let joint_count = cur.u32()? as usize;
let mut joints_out = Vec::with_capacity(joint_count);
for _ in 0..joint_count {
let name_len = cur.u32()? as usize;
let name = read_name(&mut cur, name_len, "joint name")?;
let parent = cur.i32()?;
let mut t = [0f32; 3];
for x in &mut t {
*x = cur.f32()?;
}
let mut r = [0f32; 3];
for x in &mut r {
*x = cur.f32()?;
}
let mut s = [0f32; 3];
for x in &mut s {
*x = cur.f32()?;
}
joints_out.push(PayloadJoint {
name,
parent,
translation: t,
rotation_deg: r,
scale: s,
});
}
let mut morphs = PayloadMorphs::default();
if cur.peek(MORPH_MAGIC) {
cur.skip(4)?;
let target_count = cur.u32()? as usize;
for _ in 0..target_count {
let name_len = cur.u32()? as usize;
morphs
.names
.push(read_name(&mut cur, name_len, "morph target name")?);
}
let entry_count = cur.u32()? as usize;
let block = cur.take(checked_product("morph offsets", &[vertex_count + 1, 4])?)?;
morphs
.offsets
.extend(block.chunks_exact(4).map(|c| chunk_u32(c, 0)));
let block = cur.take(checked_product("morph entries", &[entry_count, 28])?)?;
morphs.entries.extend(block.chunks_exact(28).map(|e| {
let f = |i: usize| chunk_f32(e, i * 4);
MorphEntry {
target: chunk_u32(e, 0),
position: [f(1), f(2), f(3)],
normal: [f(4), f(5), f(6)],
}
}));
morphs
.validate()
.map_err(|e| format!("skinned mesh payload morph block: {e}"))?;
}
let mut alternates: Vec<(f32, Vec<u16>)> = Vec::new();
if cur.peek(LODS_MAGIC) {
cur.skip(4)?;
let alt_count = cur.u32()? as usize;
alternates.reserve(alt_count);
for _ in 0..alt_count {
let distance = cur.f32()?;
let n = cur.u32()? as usize;
let alt = read_indices(&mut cur, n, "LOD indices")?;
alternates.push((distance, alt));
}
}
Ok(SkinnedPayload {
vertices,
indices,
joints: joints_out,
morphs,
lods: alternates,
})
}
pub fn deserialise_with_lods(bytes: &[u8]) -> Result<DeserialisedStatic, String> {
let mut cur = ByteReader::new(bytes, "mesh payload");
let vertex_count = cur.u32()? as usize;
let vertices = read_vertices(&mut cur, vertex_count)?;
let index_count = cur.u32()? as usize;
let indices = read_indices(&mut cur, index_count, "indices")?;
let mut alternates = Vec::new();
if cur.peek(LODS_MAGIC) {
cur.skip(4)?;
let alt_count = cur.u32()? as usize;
alternates.reserve(alt_count);
for _ in 0..alt_count {
let distance = cur.f32()?;
let n = cur.u32()? as usize;
let alt = read_indices(&mut cur, n, "LOD indices")?;
alternates.push((distance, alt));
}
}
Ok((vertices, indices, alternates))
}
fn read_skinned_vertices(
cur: &mut ByteReader<'_>,
count: usize,
) -> Result<Vec<SkinnedVertex>, String> {
let block = cur.take(checked_product("skinned vertices", &[count, 80])?)?;
Ok(block
.chunks_exact(80)
.map(|v| {
let f = |i: usize| chunk_f32(v, i * 4);
let j = |i: usize| chunk_u16(v, 56 + i * 2);
let w = |i: usize| chunk_f32(v, 64 + i * 4);
SkinnedVertex {
pos: [f(0), f(1), f(2)],
normal: [f(3), f(4), f(5)],
tangent: [f(6), f(7), f(8)],
color: [f(9), f(10), f(11)],
uv: [f(12), f(13)],
joints: [j(0), j(1), j(2), j(3)],
weights: [w(0), w(1), w(2), w(3)],
}
})
.collect())
}
fn read_vertices(cur: &mut ByteReader<'_>, count: usize) -> Result<Vec<Vertex>, String> {
let block = cur.take(checked_product("vertices", &[count, 56])?)?;
Ok(block
.chunks_exact(56)
.map(|v| {
let f = |i: usize| chunk_f32(v, i * 4);
Vertex {
pos: [f(0), f(1), f(2)],
normal: [f(3), f(4), f(5)],
tangent: [f(6), f(7), f(8)],
color: [f(9), f(10), f(11)],
uv: [f(12), f(13)],
}
})
.collect())
}
#[cfg(test)]
pub fn deserialise(bytes: &[u8]) -> Result<(Vec<Vertex>, Vec<u16>), String> {
let (vertices, indices, _) = deserialise_with_lods(bytes)?;
Ok((vertices, indices))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn sample_skinned() -> Vec<SkinnedVertex> {
vec![
SkinnedVertex {
pos: [1.0, 2.0, 3.0],
normal: [0.0, 1.0, 0.0],
tangent: [1.0, 0.0, 0.0],
color: [0.5, 0.6, 0.7],
uv: [0.25, 0.75],
joints: [0, 1, 2, 3],
weights: [0.5, 0.3, 0.2, 0.0],
},
SkinnedVertex {
pos: [-4.0, 5.0, -6.0],
normal: [0.0, 0.0, 1.0],
tangent: [0.0, 1.0, 0.0],
color: [1.0, 1.0, 1.0],
uv: [0.0, 1.0],
joints: [7, 0, 0, 0],
weights: [1.0, 0.0, 0.0, 0.0],
},
]
}
fn sample_skeleton() -> Vec<PayloadJoint> {
vec![
PayloadJoint {
name: "root".to_string(),
parent: -1,
translation: [0.0, 0.0, 0.0],
rotation_deg: [0.0, 0.0, 0.0],
scale: [1.0, 1.0, 1.0],
},
PayloadJoint {
name: "tip".to_string(),
parent: 0,
translation: [0.0, 1.0, 0.0],
rotation_deg: [0.0, 0.0, 0.0],
scale: [1.0, 1.0, 1.0],
},
]
}
#[test]
fn skinned_roundtrip_preserves_data() {
let verts = sample_skinned();
let idxs = vec![0u16, 1, 0];
let skel = sample_skeleton();
let bytes = serialise_skinned(&verts, &idxs, &skel);
let (out_v, out_i, out_s) = deserialise_skinned(&bytes).expect("deserialise");
assert_eq!(out_v, verts);
assert_eq!(out_i, idxs);
assert_eq!(out_s, skel);
}
#[test]
fn skinned_roundtrip_with_empty_skeleton_keeps_trailer_present() {
let verts = sample_skinned();
let idxs = vec![0u16, 1, 0];
let bytes = serialise_skinned(&verts, &idxs, &[]);
let (out_v, out_i, out_s) = deserialise_skinned(&bytes).expect("deserialise");
assert_eq!(out_v, verts);
assert_eq!(out_i, idxs);
assert!(out_s.is_empty());
}
#[test]
fn skinned_payload_size_is_predictable() {
let skel = sample_skeleton();
let bytes = serialise_skinned(&sample_skinned(), &[0u16, 1, 0], &skel);
let per_joint = skel
.iter()
.map(|j| 4 + j.name.len() + 4 + 12 + 12 + 12)
.sum::<usize>();
assert_eq!(bytes.len(), 4 + 4 + 2 * 80 + 4 + 3 * 2 + 4 + per_joint);
}
#[test]
fn vertex_layout_matches_msl() {
use core::mem::{offset_of, size_of};
assert_eq!(size_of::<Vertex>(), 56);
assert_eq!(offset_of!(Vertex, pos), 0);
assert_eq!(offset_of!(Vertex, normal), 12);
assert_eq!(offset_of!(Vertex, tangent), 24);
assert_eq!(offset_of!(Vertex, color), 36);
assert_eq!(offset_of!(Vertex, uv), 48);
}
#[test]
fn skinned_vertex_layout_matches_msl() {
use core::mem::{offset_of, size_of};
assert_eq!(size_of::<SkinnedVertex>(), 80);
assert_eq!(offset_of!(SkinnedVertex, pos), 0);
assert_eq!(offset_of!(SkinnedVertex, normal), 12);
assert_eq!(offset_of!(SkinnedVertex, tangent), 24);
assert_eq!(offset_of!(SkinnedVertex, color), 36);
assert_eq!(offset_of!(SkinnedVertex, uv), 48);
assert_eq!(offset_of!(SkinnedVertex, joints), 56);
assert_eq!(offset_of!(SkinnedVertex, weights), 64);
}
#[test]
fn deserialise_skinned_rejects_missing_magic() {
let static_bytes = serialise(&[([0.0; 3], [0.0; 3], [0.0; 3], [1.0; 3], [0.0; 2])], &[]);
assert!(deserialise_skinned(&static_bytes).is_err());
}
fn sample_skinned_vertex(pos: [f32; 3]) -> SkinnedVertex {
SkinnedVertex {
pos,
normal: [0.0, 1.0, 0.0],
tangent: [1.0, 0.0, 0.0],
color: [1.0; 3],
uv: [0.0, 0.0],
joints: [0; 4],
weights: [1.0, 0.0, 0.0, 0.0],
}
}
#[test]
fn skinned_payload_round_trips_the_morph_block() {
let vertices = vec![
sample_skinned_vertex([0.0, 0.0, 0.0]),
sample_skinned_vertex([1.0, 0.0, 0.0]),
];
let joints = vec![PayloadJoint {
name: "root".to_string(),
parent: -1,
translation: [0.0; 3],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
}];
let dense = vec![
MorphDelta {
position: [0.1, 0.2, 0.3],
normal: [0.0, 0.0, 1.0],
},
MorphDelta::default(),
MorphDelta::default(),
MorphDelta {
position: [-0.5, 0.0, 0.0],
normal: [0.0, 1.0, 0.0],
},
];
let morphs =
PayloadMorphs::from_dense(vec!["smile".to_string(), "blink".to_string()], 2, &dense)
.expect("sparse");
assert_eq!(
morphs.entries.len(),
2,
"only the two non-zero deltas are stored"
);
let lods = vec![(9.0_f32, vec![0u16, 1, 0])];
let bytes = serialise_skinned_with_lods(&vertices, &[0, 1, 0], &joints, &morphs, &lods);
let p = deserialise_skinned_with_lods(&bytes).expect("deserialise");
assert_eq!(p.vertices.len(), 2);
assert_eq!(p.joints.len(), 1);
assert_eq!(p.morphs, morphs, "morph block must round-trip exactly");
assert_eq!(
p.morphs.to_dense(),
dense,
"sparse block expands to the source"
);
assert_eq!(p.lods.len(), 1, "LOD trailer must survive after MRPS");
assert_eq!(p.lods[0].1, vec![0u16, 1, 0]);
}
#[test]
fn a_morph_block_whose_tables_disagree_is_rejected() {
let vertices = vec![sample_skinned_vertex([0.0, 0.0, 0.0])];
let morphs = PayloadMorphs {
names: vec!["t".to_string()],
offsets: vec![0, 1],
entries: vec![MorphEntry {
target: 3,
position: [1.0, 0.0, 0.0],
normal: [0.0; 3],
}],
};
let bytes = serialise_skinned_with_lods(&vertices, &[0, 0, 0], &[], &morphs, &[]);
let err = deserialise_skinned_with_lods(&bytes).unwrap_err();
assert!(err.contains("morph block"), "{err}");
assert!(err.contains("target 3 of 1"), "{err}");
}
#[test]
fn skinned_payload_without_morphs_is_byte_identical_to_legacy() {
let vertices = vec![sample_skinned_vertex([0.0, 0.0, 0.0])];
let legacy = serialise_skinned(&vertices, &[0, 0, 0], &[]);
let with_empty =
serialise_skinned_with_lods(&vertices, &[0, 0, 0], &[], &PayloadMorphs::default(), &[]);
assert_eq!(legacy, with_empty, "empty morphs must add no bytes");
let p = deserialise_skinned_with_lods(&legacy).expect("deserialise");
assert!(p.morphs.is_empty());
}
fn sample_static_verts() -> Vec<VertTuple> {
vec![
(
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
[1.0; 3],
[0.0, 0.0],
),
(
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
[1.0; 3],
[1.0, 0.0],
),
(
[0.0, 0.0, 1.0],
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
[1.0; 3],
[0.0, 1.0],
),
]
}
#[test]
fn serialise_with_no_lods_matches_legacy_format() {
let verts = sample_static_verts();
let idx = vec![0u16, 1, 2];
let legacy = serialise(&verts, &idx);
let with_lods = serialise_with_lods(&verts, &idx, &[]);
assert_eq!(legacy, with_lods, "no alternates → no trailer bytes");
}
#[test]
fn lod_trailer_roundtrip_preserves_distances_and_indices() {
let verts = sample_static_verts();
let lod0 = vec![0u16, 1, 2];
let alternates = vec![(8.0_f32, vec![0u16, 2, 1]), (25.0_f32, vec![0u16, 1, 2])];
let bytes = serialise_with_lods(&verts, &lod0, &alternates);
let (out_v, out_idx, out_alts) = deserialise_with_lods(&bytes).expect("deserialise");
assert_eq!(out_v.len(), verts.len());
assert_eq!(out_idx, lod0);
assert_eq!(out_alts.len(), 2);
assert_eq!(out_alts[0].0, 8.0);
assert_eq!(out_alts[0].1, vec![0u16, 2, 1]);
assert_eq!(out_alts[1].0, 25.0);
assert_eq!(out_alts[1].1, vec![0u16, 1, 2]);
}
#[test]
fn legacy_payload_has_no_alternates() {
let verts = sample_static_verts();
let idx = vec![0u16, 1, 2];
let bytes = serialise(&verts, &idx);
let (_, _, alts) = deserialise_with_lods(&bytes).expect("deserialise");
assert!(alts.is_empty());
}
#[test]
fn heightfield_trailer_roundtrips_without_lods() {
let verts = sample_static_verts();
let idx = vec![0u16, 1, 2];
let heights = vec![0.0f32, 1.0, 2.0, 3.0];
let mut bytes = serialise_with_lods(&verts, &idx, &[]);
bytes.extend_from_slice(&serialise_heightfield_trailer(2, 2, &heights));
let grid = deserialise_heightfield(&bytes)
.expect("parse")
.expect("trailer present");
assert_eq!(grid.rows, 2);
assert_eq!(grid.cols, 2);
assert_eq!(grid.heights, heights);
let (out_v, out_i, out_alts) = deserialise_with_lods(&bytes).expect("render path");
assert_eq!(out_v.len(), verts.len());
assert_eq!(out_i, idx);
assert!(out_alts.is_empty());
}
#[test]
fn heightfield_trailer_roundtrips_after_lod_trailer() {
let verts = sample_static_verts();
let lod0 = vec![0u16, 1, 2];
let alternates = vec![(8.0_f32, vec![0u16, 2, 1]), (25.0_f32, vec![0u16, 1, 2])];
let heights = vec![-1.0f32, 0.5, 0.5, 1.0, 2.0, 2.5, 3.0, 3.5, 4.0];
let mut bytes = serialise_with_lods(&verts, &lod0, &alternates);
bytes.extend_from_slice(&serialise_heightfield_trailer(3, 3, &heights));
let (_, out_i, out_alts) = deserialise_with_lods(&bytes).expect("render path");
assert_eq!(out_i, lod0);
assert_eq!(out_alts.len(), 2);
let grid = deserialise_heightfield(&bytes)
.expect("parse")
.expect("trailer present");
assert_eq!((grid.rows, grid.cols), (3, 3));
assert_eq!(grid.heights, heights);
}
#[test]
fn a_heightfield_trailer_whose_footprint_overflows_is_rejected() {
let verts = sample_static_verts();
let mut bytes = serialise_with_lods(&verts, &[0u16, 1, 2], &[]);
bytes.extend_from_slice(HFLD_MAGIC);
bytes.extend_from_slice(&0x8000_0000u32.to_le_bytes());
bytes.extend_from_slice(&0x8000_0000u32.to_le_bytes());
let err = match deserialise_heightfield(&bytes) {
Err(e) => e,
Ok(_) => panic!("an overflowing grid must be rejected"),
};
assert!(err.contains("heightfield grid"), "{err}");
}
#[test]
fn no_heightfield_trailer_returns_none() {
let verts = sample_static_verts();
let bytes = serialise_with_lods(&verts, &[0u16, 1, 2], &[(10.0, vec![0u16, 2, 1])]);
assert!(deserialise_heightfield(&bytes).expect("parse").is_none());
}
#[test]
fn legacy_deserialise_still_works_on_multi_lod_payload() {
let verts = sample_static_verts();
let lod0 = vec![0u16, 1, 2];
let bytes = serialise_with_lods(&verts, &lod0, &[(10.0, vec![0u16, 2, 1])]);
let (out_v, out_idx) = deserialise(&bytes).expect("legacy reader");
assert_eq!(out_v.len(), verts.len());
assert_eq!(out_idx, lod0);
}
}