1use crate::decode::{ByteReader, checked_product};
29use alloc::format;
30use alloc::string::String;
31use alloc::string::ToString;
32use alloc::vec::Vec;
33
34fn chunk_f32(chunk: &[u8], at: usize) -> f32 {
36 f32::from_le_bytes([chunk[at], chunk[at + 1], chunk[at + 2], chunk[at + 3]])
37}
38
39fn chunk_u32(chunk: &[u8], at: usize) -> u32 {
41 u32::from_le_bytes([chunk[at], chunk[at + 1], chunk[at + 2], chunk[at + 3]])
42}
43
44fn chunk_u16(chunk: &[u8], at: usize) -> u16 {
46 u16::from_le_bytes([chunk[at], chunk[at + 1]])
47}
48
49fn read_name(cur: &mut ByteReader<'_>, len: usize, what: &str) -> Result<String, String> {
51 core::str::from_utf8(cur.take(len)?)
52 .map_err(|e| format!("{what} is not valid utf-8: {e}"))
53 .map(str::to_string)
54}
55
56fn read_indices(cur: &mut ByteReader<'_>, n: usize, what: &str) -> Result<Vec<u16>, String> {
58 let block = cur.take(checked_product(what, &[n, 2])?)?;
59 Ok(block.chunks_exact(2).map(|c| chunk_u16(c, 0)).collect())
60}
61
62#[derive(Copy, Clone, Debug, bytemuck::NoUninit)]
65#[repr(C)]
66pub struct Vertex {
67 pub pos: [f32; 3],
69 pub normal: [f32; 3],
72 pub tangent: [f32; 3],
76 pub color: [f32; 3],
78 pub uv: [f32; 2],
80}
81
82type VertTuple = ([f32; 3], [f32; 3], [f32; 3], [f32; 3], [f32; 2]);
85
86type LodAlternates = Vec<(f32, Vec<u16>)>;
88
89type DeserialisedStatic = (Vec<Vertex>, Vec<u16>, LodAlternates);
91
92type DeserialisedSkinned = (Vec<SkinnedVertex>, Vec<u16>, Vec<PayloadJoint>);
94
95#[derive(Clone, Debug, Default)]
98pub struct SkinnedPayload {
99 pub vertices: Vec<SkinnedVertex>,
101 pub indices: Vec<u16>,
103 pub joints: Vec<PayloadJoint>,
105 pub morphs: PayloadMorphs,
107 pub lods: LodAlternates,
109}
110
111pub fn serialise(vertices: &[VertTuple], indices: &[u16]) -> Vec<u8> {
114 let mut buf = Vec::with_capacity(4 + vertices.len() * 56 + 4 + indices.len() * 2);
115 buf.extend_from_slice(&(vertices.len() as u32).to_le_bytes());
116 for (pos, normal, tangent, color, uv) in vertices {
117 for x in pos
118 .iter()
119 .chain(normal.iter())
120 .chain(tangent.iter())
121 .chain(color.iter())
122 .chain(uv.iter())
123 {
124 buf.extend_from_slice(&x.to_le_bytes());
125 }
126 }
127 buf.extend_from_slice(&(indices.len() as u32).to_le_bytes());
128 for i in indices {
129 buf.extend_from_slice(&i.to_le_bytes());
130 }
131 buf
132}
133
134const LODS_MAGIC: &[u8; 4] = b"LODS";
137
138pub fn serialise_with_lods(
144 vertices: &[VertTuple],
145 indices: &[u16],
146 lod_alternates: &[(f32, Vec<u16>)],
147) -> Vec<u8> {
148 let mut buf = serialise(vertices, indices);
149 if lod_alternates.is_empty() {
150 return buf;
151 }
152 buf.extend_from_slice(LODS_MAGIC);
153 buf.extend_from_slice(&(lod_alternates.len() as u32).to_le_bytes());
154 for (distance, idx) in lod_alternates {
155 buf.extend_from_slice(&distance.to_le_bytes());
156 buf.extend_from_slice(&(idx.len() as u32).to_le_bytes());
157 for i in idx {
158 buf.extend_from_slice(&i.to_le_bytes());
159 }
160 }
161 buf
162}
163
164const HFLD_MAGIC: &[u8; 4] = b"HFLD";
171
172pub struct HeightfieldGrid {
176 pub rows: usize,
178 pub cols: usize,
180 pub heights: Vec<f32>,
182}
183
184pub fn serialise_heightfield_trailer(rows: usize, cols: usize, heights: &[f32]) -> Vec<u8> {
189 let mut buf = Vec::with_capacity(4 + 4 + 4 + heights.len() * 4);
190 buf.extend_from_slice(HFLD_MAGIC);
191 buf.extend_from_slice(&(rows as u32).to_le_bytes());
192 buf.extend_from_slice(&(cols as u32).to_le_bytes());
193 for h in heights {
194 buf.extend_from_slice(&h.to_le_bytes());
195 }
196 buf
197}
198
199pub fn deserialise_heightfield(bytes: &[u8]) -> Result<Option<HeightfieldGrid>, String> {
205 let mut cur = ByteReader::new(bytes, "mesh payload");
206
207 let vertex_count = cur.u32()? as usize;
209 cur.skip(checked_product("vertices", &[vertex_count, 56])?)?;
210 let index_count = cur.u32()? as usize;
211 cur.skip(checked_product("indices", &[index_count, 2])?)?;
212
213 if cur.peek(LODS_MAGIC) {
216 cur.skip(4)?;
217 let alt_count = cur.u32()? as usize;
218 for _ in 0..alt_count {
219 cur.skip(4)?; let n = cur.u32()? as usize;
221 cur.skip(checked_product("lod indices", &[n, 2])?)?;
222 }
223 }
224
225 if !cur.peek(HFLD_MAGIC) {
227 return Ok(None);
228 }
229 cur.skip(4)?;
230 let rows = cur.u32()? as usize;
231 let cols = cur.u32()? as usize;
232 let count = checked_product("heightfield grid", &[rows, cols])?;
233 let block = cur
234 .take(checked_product("heightfield grid", &[count, 4])?)
235 .map_err(|_| format!("heightfield trailer too short for {rows} x {cols} grid"))?;
236 let heights = block.chunks_exact(4).map(|h| chunk_f32(h, 0)).collect();
237 Ok(Some(HeightfieldGrid {
238 rows,
239 cols,
240 heights,
241 }))
242}
243
244#[derive(Copy, Clone, Debug, PartialEq, bytemuck::NoUninit)]
253#[repr(C)]
254pub struct SkinnedVertex {
255 pub pos: [f32; 3],
257 pub normal: [f32; 3],
259 pub tangent: [f32; 3],
261 pub color: [f32; 3],
263 pub uv: [f32; 2],
265 pub joints: [u16; 4],
267 pub weights: [f32; 4],
269}
270
271const SKINNED_MAGIC: &[u8; 4] = b"SKMV";
275
276const MORPH_MAGIC: &[u8; 4] = b"MRPS";
278
279pub use super::morph_targets::{MORPH_DELTA_EPSILON, MorphDelta, MorphEntry, PayloadMorphs};
280
281#[derive(Clone, Debug, PartialEq)]
288pub struct PayloadJoint {
289 pub name: String,
291 pub parent: i32,
293 pub translation: [f32; 3],
295 pub rotation_deg: [f32; 3],
297 pub scale: [f32; 3],
299}
300
301#[cfg(test)]
319pub(crate) fn serialise_skinned(
320 vertices: &[SkinnedVertex],
321 indices: &[u16],
322 joints: &[PayloadJoint],
323) -> Vec<u8> {
324 serialise_skinned_with_lods(vertices, indices, joints, &PayloadMorphs::default(), &[])
325}
326
327pub fn serialise_skinned_with_lods(
336 vertices: &[SkinnedVertex],
337 indices: &[u16],
338 joints: &[PayloadJoint],
339 morphs: &PayloadMorphs,
340 lod_alternates: &[(f32, Vec<u16>)],
341) -> Vec<u8> {
342 let mut buf = Vec::with_capacity(4 + 4 + vertices.len() * 80 + 4 + indices.len() * 2 + 4);
343 buf.extend_from_slice(SKINNED_MAGIC);
344 buf.extend_from_slice(&(vertices.len() as u32).to_le_bytes());
345 for v in vertices {
346 for f in v
347 .pos
348 .iter()
349 .chain(v.normal.iter())
350 .chain(v.tangent.iter())
351 .chain(v.color.iter())
352 .chain(v.uv.iter())
353 {
354 buf.extend_from_slice(&f.to_le_bytes());
355 }
356 for j in v.joints {
357 buf.extend_from_slice(&j.to_le_bytes());
358 }
359 for w in v.weights {
360 buf.extend_from_slice(&w.to_le_bytes());
361 }
362 }
363 buf.extend_from_slice(&(indices.len() as u32).to_le_bytes());
364 for i in indices {
365 buf.extend_from_slice(&i.to_le_bytes());
366 }
367 buf.extend_from_slice(&(joints.len() as u32).to_le_bytes());
368 for j in joints {
369 let name_bytes = j.name.as_bytes();
370 buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
371 buf.extend_from_slice(name_bytes);
372 buf.extend_from_slice(&j.parent.to_le_bytes());
373 for x in j
374 .translation
375 .iter()
376 .chain(j.rotation_deg.iter())
377 .chain(j.scale.iter())
378 {
379 buf.extend_from_slice(&x.to_le_bytes());
380 }
381 }
382 if !morphs.is_empty() {
383 buf.extend_from_slice(MORPH_MAGIC);
384 buf.extend_from_slice(&(morphs.names.len() as u32).to_le_bytes());
385 for name in &morphs.names {
386 let name_bytes = name.as_bytes();
387 buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
388 buf.extend_from_slice(name_bytes);
389 }
390 buf.extend_from_slice(&(morphs.entries.len() as u32).to_le_bytes());
391 for o in &morphs.offsets {
392 buf.extend_from_slice(&o.to_le_bytes());
393 }
394 for e in &morphs.entries {
395 buf.extend_from_slice(&e.target.to_le_bytes());
396 for x in e.position.iter().chain(e.normal.iter()) {
397 buf.extend_from_slice(&x.to_le_bytes());
398 }
399 }
400 }
401 if !lod_alternates.is_empty() {
402 buf.extend_from_slice(LODS_MAGIC);
403 buf.extend_from_slice(&(lod_alternates.len() as u32).to_le_bytes());
404 for (distance, idx) in lod_alternates {
405 buf.extend_from_slice(&distance.to_le_bytes());
406 buf.extend_from_slice(&(idx.len() as u32).to_le_bytes());
407 for i in idx {
408 buf.extend_from_slice(&i.to_le_bytes());
409 }
410 }
411 }
412 buf
413}
414
415pub fn deserialise_skinned(bytes: &[u8]) -> Result<DeserialisedSkinned, String> {
420 let p = deserialise_skinned_with_lods(bytes)?;
421 Ok((p.vertices, p.indices, p.joints))
422}
423
424pub fn deserialise_skinned_with_lods(bytes: &[u8]) -> Result<SkinnedPayload, String> {
429 if bytes.len() < 8 || &bytes[0..4] != SKINNED_MAGIC {
430 return Err("skinned mesh payload missing SKMV magic header".to_string());
431 }
432 let mut cur = ByteReader::new(bytes, "skinned mesh payload");
433 cur.skip(4)?;
434
435 let vertex_count = cur.u32()? as usize;
436 let vertices = read_skinned_vertices(&mut cur, vertex_count)?;
437
438 let index_count = cur.u32()? as usize;
439 let indices = read_indices(&mut cur, index_count, "indices")?;
440
441 let joint_count = cur.u32()? as usize;
442 let mut joints_out = Vec::with_capacity(joint_count);
443 for _ in 0..joint_count {
444 let name_len = cur.u32()? as usize;
445 let name = read_name(&mut cur, name_len, "joint name")?;
446 let parent = cur.i32()?;
447 let mut t = [0f32; 3];
448 for x in &mut t {
449 *x = cur.f32()?;
450 }
451 let mut r = [0f32; 3];
452 for x in &mut r {
453 *x = cur.f32()?;
454 }
455 let mut s = [0f32; 3];
456 for x in &mut s {
457 *x = cur.f32()?;
458 }
459 joints_out.push(PayloadJoint {
460 name,
461 parent,
462 translation: t,
463 rotation_deg: r,
464 scale: s,
465 });
466 }
467
468 let mut morphs = PayloadMorphs::default();
470 if cur.peek(MORPH_MAGIC) {
471 cur.skip(4)?;
472 let target_count = cur.u32()? as usize;
473 for _ in 0..target_count {
474 let name_len = cur.u32()? as usize;
475 morphs
476 .names
477 .push(read_name(&mut cur, name_len, "morph target name")?);
478 }
479 let entry_count = cur.u32()? as usize;
480 let block = cur.take(checked_product("morph offsets", &[vertex_count + 1, 4])?)?;
481 morphs
482 .offsets
483 .extend(block.chunks_exact(4).map(|c| chunk_u32(c, 0)));
484 let block = cur.take(checked_product("morph entries", &[entry_count, 28])?)?;
485 morphs.entries.extend(block.chunks_exact(28).map(|e| {
486 let f = |i: usize| chunk_f32(e, i * 4);
487 MorphEntry {
488 target: chunk_u32(e, 0),
489 position: [f(1), f(2), f(3)],
490 normal: [f(4), f(5), f(6)],
491 }
492 }));
493 morphs
494 .validate()
495 .map_err(|e| format!("skinned mesh payload morph block: {e}"))?;
496 }
497
498 let mut alternates: Vec<(f32, Vec<u16>)> = Vec::new();
502 if cur.peek(LODS_MAGIC) {
503 cur.skip(4)?;
504 let alt_count = cur.u32()? as usize;
505 alternates.reserve(alt_count);
506 for _ in 0..alt_count {
507 let distance = cur.f32()?;
508 let n = cur.u32()? as usize;
509 let alt = read_indices(&mut cur, n, "LOD indices")?;
510 alternates.push((distance, alt));
511 }
512 }
513
514 Ok(SkinnedPayload {
515 vertices,
516 indices,
517 joints: joints_out,
518 morphs,
519 lods: alternates,
520 })
521}
522
523pub fn deserialise_with_lods(bytes: &[u8]) -> Result<DeserialisedStatic, String> {
530 let mut cur = ByteReader::new(bytes, "mesh payload");
531
532 let vertex_count = cur.u32()? as usize;
533 let vertices = read_vertices(&mut cur, vertex_count)?;
534
535 let index_count = cur.u32()? as usize;
536 let indices = read_indices(&mut cur, index_count, "indices")?;
537
538 let mut alternates = Vec::new();
541 if cur.peek(LODS_MAGIC) {
542 cur.skip(4)?;
543 let alt_count = cur.u32()? as usize;
544 alternates.reserve(alt_count);
545 for _ in 0..alt_count {
546 let distance = cur.f32()?;
547 let n = cur.u32()? as usize;
548 let alt = read_indices(&mut cur, n, "LOD indices")?;
549 alternates.push((distance, alt));
550 }
551 }
552
553 Ok((vertices, indices, alternates))
554}
555
556fn read_skinned_vertices(
559 cur: &mut ByteReader<'_>,
560 count: usize,
561) -> Result<Vec<SkinnedVertex>, String> {
562 let block = cur.take(checked_product("skinned vertices", &[count, 80])?)?;
563 Ok(block
564 .chunks_exact(80)
565 .map(|v| {
566 let f = |i: usize| chunk_f32(v, i * 4);
567 let j = |i: usize| chunk_u16(v, 56 + i * 2);
568 let w = |i: usize| chunk_f32(v, 64 + i * 4);
569 SkinnedVertex {
570 pos: [f(0), f(1), f(2)],
571 normal: [f(3), f(4), f(5)],
572 tangent: [f(6), f(7), f(8)],
573 color: [f(9), f(10), f(11)],
574 uv: [f(12), f(13)],
575 joints: [j(0), j(1), j(2), j(3)],
576 weights: [w(0), w(1), w(2), w(3)],
577 }
578 })
579 .collect())
580}
581
582fn read_vertices(cur: &mut ByteReader<'_>, count: usize) -> Result<Vec<Vertex>, String> {
585 let block = cur.take(checked_product("vertices", &[count, 56])?)?;
586 Ok(block
587 .chunks_exact(56)
588 .map(|v| {
589 let f = |i: usize| chunk_f32(v, i * 4);
590 Vertex {
591 pos: [f(0), f(1), f(2)],
592 normal: [f(3), f(4), f(5)],
593 tangent: [f(6), f(7), f(8)],
594 color: [f(9), f(10), f(11)],
595 uv: [f(12), f(13)],
596 }
597 })
598 .collect())
599}
600#[cfg(test)]
603pub fn deserialise(bytes: &[u8]) -> Result<(Vec<Vertex>, Vec<u16>), String> {
604 let (vertices, indices, _) = deserialise_with_lods(bytes)?;
605 Ok((vertices, indices))
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use alloc::vec;
612
613 fn sample_skinned() -> Vec<SkinnedVertex> {
614 vec![
615 SkinnedVertex {
616 pos: [1.0, 2.0, 3.0],
617 normal: [0.0, 1.0, 0.0],
618 tangent: [1.0, 0.0, 0.0],
619 color: [0.5, 0.6, 0.7],
620 uv: [0.25, 0.75],
621 joints: [0, 1, 2, 3],
622 weights: [0.5, 0.3, 0.2, 0.0],
623 },
624 SkinnedVertex {
625 pos: [-4.0, 5.0, -6.0],
626 normal: [0.0, 0.0, 1.0],
627 tangent: [0.0, 1.0, 0.0],
628 color: [1.0, 1.0, 1.0],
629 uv: [0.0, 1.0],
630 joints: [7, 0, 0, 0],
631 weights: [1.0, 0.0, 0.0, 0.0],
632 },
633 ]
634 }
635
636 fn sample_skeleton() -> Vec<PayloadJoint> {
637 vec![
638 PayloadJoint {
639 name: "root".to_string(),
640 parent: -1,
641 translation: [0.0, 0.0, 0.0],
642 rotation_deg: [0.0, 0.0, 0.0],
643 scale: [1.0, 1.0, 1.0],
644 },
645 PayloadJoint {
646 name: "tip".to_string(),
647 parent: 0,
648 translation: [0.0, 1.0, 0.0],
649 rotation_deg: [0.0, 0.0, 0.0],
650 scale: [1.0, 1.0, 1.0],
651 },
652 ]
653 }
654
655 #[test]
656 fn skinned_roundtrip_preserves_data() {
657 let verts = sample_skinned();
658 let idxs = vec![0u16, 1, 0];
659 let skel = sample_skeleton();
660 let bytes = serialise_skinned(&verts, &idxs, &skel);
661 let (out_v, out_i, out_s) = deserialise_skinned(&bytes).expect("deserialise");
662 assert_eq!(out_v, verts);
663 assert_eq!(out_i, idxs);
664 assert_eq!(out_s, skel);
665 }
666
667 #[test]
668 fn skinned_roundtrip_with_empty_skeleton_keeps_trailer_present() {
669 let verts = sample_skinned();
672 let idxs = vec![0u16, 1, 0];
673 let bytes = serialise_skinned(&verts, &idxs, &[]);
674 let (out_v, out_i, out_s) = deserialise_skinned(&bytes).expect("deserialise");
675 assert_eq!(out_v, verts);
676 assert_eq!(out_i, idxs);
677 assert!(out_s.is_empty());
678 }
679
680 #[test]
681 fn skinned_payload_size_is_predictable() {
682 let skel = sample_skeleton();
685 let bytes = serialise_skinned(&sample_skinned(), &[0u16, 1, 0], &skel);
686 let per_joint = skel
687 .iter()
688 .map(|j| 4 + j.name.len() + 4 + 12 + 12 + 12)
689 .sum::<usize>();
690 assert_eq!(bytes.len(), 4 + 4 + 2 * 80 + 4 + 3 * 2 + 4 + per_joint);
691 }
692
693 #[test]
694 fn vertex_layout_matches_msl() {
695 use core::mem::{offset_of, size_of};
701 assert_eq!(size_of::<Vertex>(), 56);
702 assert_eq!(offset_of!(Vertex, pos), 0);
703 assert_eq!(offset_of!(Vertex, normal), 12);
704 assert_eq!(offset_of!(Vertex, tangent), 24);
705 assert_eq!(offset_of!(Vertex, color), 36);
706 assert_eq!(offset_of!(Vertex, uv), 48);
707 }
708
709 #[test]
710 fn skinned_vertex_layout_matches_msl() {
711 use core::mem::{offset_of, size_of};
717 assert_eq!(size_of::<SkinnedVertex>(), 80);
718 assert_eq!(offset_of!(SkinnedVertex, pos), 0);
719 assert_eq!(offset_of!(SkinnedVertex, normal), 12);
720 assert_eq!(offset_of!(SkinnedVertex, tangent), 24);
721 assert_eq!(offset_of!(SkinnedVertex, color), 36);
722 assert_eq!(offset_of!(SkinnedVertex, uv), 48);
723 assert_eq!(offset_of!(SkinnedVertex, joints), 56);
724 assert_eq!(offset_of!(SkinnedVertex, weights), 64);
725 }
726
727 #[test]
728 fn deserialise_skinned_rejects_missing_magic() {
729 let static_bytes = serialise(&[([0.0; 3], [0.0; 3], [0.0; 3], [1.0; 3], [0.0; 2])], &[]);
732 assert!(deserialise_skinned(&static_bytes).is_err());
733 }
734
735 fn sample_skinned_vertex(pos: [f32; 3]) -> SkinnedVertex {
736 SkinnedVertex {
737 pos,
738 normal: [0.0, 1.0, 0.0],
739 tangent: [1.0, 0.0, 0.0],
740 color: [1.0; 3],
741 uv: [0.0, 0.0],
742 joints: [0; 4],
743 weights: [1.0, 0.0, 0.0, 0.0],
744 }
745 }
746
747 #[test]
748 fn skinned_payload_round_trips_the_morph_block() {
749 let vertices = vec![
750 sample_skinned_vertex([0.0, 0.0, 0.0]),
751 sample_skinned_vertex([1.0, 0.0, 0.0]),
752 ];
753 let joints = vec![PayloadJoint {
754 name: "root".to_string(),
755 parent: -1,
756 translation: [0.0; 3],
757 rotation_deg: [0.0; 3],
758 scale: [1.0; 3],
759 }];
760 let dense = vec![
761 MorphDelta {
762 position: [0.1, 0.2, 0.3],
763 normal: [0.0, 0.0, 1.0],
764 },
765 MorphDelta::default(),
766 MorphDelta::default(),
767 MorphDelta {
768 position: [-0.5, 0.0, 0.0],
769 normal: [0.0, 1.0, 0.0],
770 },
771 ];
772 let morphs =
773 PayloadMorphs::from_dense(vec!["smile".to_string(), "blink".to_string()], 2, &dense)
774 .expect("sparse");
775 assert_eq!(
776 morphs.entries.len(),
777 2,
778 "only the two non-zero deltas are stored"
779 );
780 let lods = vec![(9.0_f32, vec![0u16, 1, 0])];
781 let bytes = serialise_skinned_with_lods(&vertices, &[0, 1, 0], &joints, &morphs, &lods);
782 let p = deserialise_skinned_with_lods(&bytes).expect("deserialise");
783 assert_eq!(p.vertices.len(), 2);
784 assert_eq!(p.joints.len(), 1);
785 assert_eq!(p.morphs, morphs, "morph block must round-trip exactly");
786 assert_eq!(
787 p.morphs.to_dense(),
788 dense,
789 "sparse block expands to the source"
790 );
791 assert_eq!(p.lods.len(), 1, "LOD trailer must survive after MRPS");
792 assert_eq!(p.lods[0].1, vec![0u16, 1, 0]);
793 }
794
795 #[test]
796 fn a_morph_block_whose_tables_disagree_is_rejected() {
797 let vertices = vec![sample_skinned_vertex([0.0, 0.0, 0.0])];
798 let morphs = PayloadMorphs {
799 names: vec!["t".to_string()],
800 offsets: vec![0, 1],
801 entries: vec![MorphEntry {
802 target: 3,
803 position: [1.0, 0.0, 0.0],
804 normal: [0.0; 3],
805 }],
806 };
807 let bytes = serialise_skinned_with_lods(&vertices, &[0, 0, 0], &[], &morphs, &[]);
808 let err = deserialise_skinned_with_lods(&bytes).unwrap_err();
809 assert!(err.contains("morph block"), "{err}");
810 assert!(err.contains("target 3 of 1"), "{err}");
811 }
812
813 #[test]
814 fn skinned_payload_without_morphs_is_byte_identical_to_legacy() {
815 let vertices = vec![sample_skinned_vertex([0.0, 0.0, 0.0])];
816 let legacy = serialise_skinned(&vertices, &[0, 0, 0], &[]);
817 let with_empty =
818 serialise_skinned_with_lods(&vertices, &[0, 0, 0], &[], &PayloadMorphs::default(), &[]);
819 assert_eq!(legacy, with_empty, "empty morphs must add no bytes");
820 let p = deserialise_skinned_with_lods(&legacy).expect("deserialise");
821 assert!(p.morphs.is_empty());
822 }
823
824 fn sample_static_verts() -> Vec<VertTuple> {
825 vec![
826 (
827 [0.0, 0.0, 0.0],
828 [0.0, 1.0, 0.0],
829 [1.0, 0.0, 0.0],
830 [1.0; 3],
831 [0.0, 0.0],
832 ),
833 (
834 [1.0, 0.0, 0.0],
835 [0.0, 1.0, 0.0],
836 [1.0, 0.0, 0.0],
837 [1.0; 3],
838 [1.0, 0.0],
839 ),
840 (
841 [0.0, 0.0, 1.0],
842 [0.0, 1.0, 0.0],
843 [1.0, 0.0, 0.0],
844 [1.0; 3],
845 [0.0, 1.0],
846 ),
847 ]
848 }
849
850 #[test]
851 fn serialise_with_no_lods_matches_legacy_format() {
852 let verts = sample_static_verts();
853 let idx = vec![0u16, 1, 2];
854 let legacy = serialise(&verts, &idx);
855 let with_lods = serialise_with_lods(&verts, &idx, &[]);
856 assert_eq!(legacy, with_lods, "no alternates → no trailer bytes");
857 }
858
859 #[test]
860 fn lod_trailer_roundtrip_preserves_distances_and_indices() {
861 let verts = sample_static_verts();
862 let lod0 = vec![0u16, 1, 2];
863 let alternates = vec![(8.0_f32, vec![0u16, 2, 1]), (25.0_f32, vec![0u16, 1, 2])];
864 let bytes = serialise_with_lods(&verts, &lod0, &alternates);
865 let (out_v, out_idx, out_alts) = deserialise_with_lods(&bytes).expect("deserialise");
866 assert_eq!(out_v.len(), verts.len());
867 assert_eq!(out_idx, lod0);
868 assert_eq!(out_alts.len(), 2);
869 assert_eq!(out_alts[0].0, 8.0);
870 assert_eq!(out_alts[0].1, vec![0u16, 2, 1]);
871 assert_eq!(out_alts[1].0, 25.0);
872 assert_eq!(out_alts[1].1, vec![0u16, 1, 2]);
873 }
874
875 #[test]
876 fn legacy_payload_has_no_alternates() {
877 let verts = sample_static_verts();
881 let idx = vec![0u16, 1, 2];
882 let bytes = serialise(&verts, &idx);
883 let (_, _, alts) = deserialise_with_lods(&bytes).expect("deserialise");
884 assert!(alts.is_empty());
885 }
886
887 #[test]
888 fn heightfield_trailer_roundtrips_without_lods() {
889 let verts = sample_static_verts();
890 let idx = vec![0u16, 1, 2];
891 let heights = vec![0.0f32, 1.0, 2.0, 3.0];
892 let mut bytes = serialise_with_lods(&verts, &idx, &[]);
893 bytes.extend_from_slice(&serialise_heightfield_trailer(2, 2, &heights));
894
895 let grid = deserialise_heightfield(&bytes)
896 .expect("parse")
897 .expect("trailer present");
898 assert_eq!(grid.rows, 2);
899 assert_eq!(grid.cols, 2);
900 assert_eq!(grid.heights, heights);
901
902 let (out_v, out_i, out_alts) = deserialise_with_lods(&bytes).expect("render path");
904 assert_eq!(out_v.len(), verts.len());
905 assert_eq!(out_i, idx);
906 assert!(out_alts.is_empty());
907 }
908
909 #[test]
910 fn heightfield_trailer_roundtrips_after_lod_trailer() {
911 let verts = sample_static_verts();
912 let lod0 = vec![0u16, 1, 2];
913 let alternates = vec![(8.0_f32, vec![0u16, 2, 1]), (25.0_f32, vec![0u16, 1, 2])];
914 let heights = vec![-1.0f32, 0.5, 0.5, 1.0, 2.0, 2.5, 3.0, 3.5, 4.0];
915 let mut bytes = serialise_with_lods(&verts, &lod0, &alternates);
916 bytes.extend_from_slice(&serialise_heightfield_trailer(3, 3, &heights));
917
918 let (_, out_i, out_alts) = deserialise_with_lods(&bytes).expect("render path");
920 assert_eq!(out_i, lod0);
921 assert_eq!(out_alts.len(), 2);
922
923 let grid = deserialise_heightfield(&bytes)
924 .expect("parse")
925 .expect("trailer present");
926 assert_eq!((grid.rows, grid.cols), (3, 3));
927 assert_eq!(grid.heights, heights);
928 }
929
930 #[test]
931 fn a_heightfield_trailer_whose_footprint_overflows_is_rejected() {
932 let verts = sample_static_verts();
938 let mut bytes = serialise_with_lods(&verts, &[0u16, 1, 2], &[]);
939 bytes.extend_from_slice(HFLD_MAGIC);
940 bytes.extend_from_slice(&0x8000_0000u32.to_le_bytes());
941 bytes.extend_from_slice(&0x8000_0000u32.to_le_bytes());
942
943 let err = match deserialise_heightfield(&bytes) {
944 Err(e) => e,
945 Ok(_) => panic!("an overflowing grid must be rejected"),
946 };
947 assert!(err.contains("heightfield grid"), "{err}");
948 }
949
950 #[test]
951 fn no_heightfield_trailer_returns_none() {
952 let verts = sample_static_verts();
953 let bytes = serialise_with_lods(&verts, &[0u16, 1, 2], &[(10.0, vec![0u16, 2, 1])]);
954 assert!(deserialise_heightfield(&bytes).expect("parse").is_none());
955 }
956
957 #[test]
958 fn legacy_deserialise_still_works_on_multi_lod_payload() {
959 let verts = sample_static_verts();
963 let lod0 = vec![0u16, 1, 2];
964 let bytes = serialise_with_lods(&verts, &lod0, &[(10.0, vec![0u16, 2, 1])]);
965 let (out_v, out_idx) = deserialise(&bytes).expect("legacy reader");
966 assert_eq!(out_v.len(), verts.len());
967 assert_eq!(out_idx, lod0);
968 }
969}