1#![allow(dead_code)]
5
6use std::io::{Read, Write};
7use std::path::Path;
8
9use oxihuman_mesh::MeshBuffers;
10
11const QMSH_MAGIC: &[u8; 4] = b"QMSH";
14const QMSH_VERSION: u32 = 1;
15
16#[derive(Debug, Clone)]
21pub struct QuantizeRange {
22 pub min: f32,
23 pub max: f32,
24}
25
26impl QuantizeRange {
27 pub fn from_data(values: &[f32]) -> Self {
31 if values.is_empty() {
32 return Self { min: 0.0, max: 1.0 };
33 }
34 let mut lo = values[0];
35 let mut hi = values[0];
36 for &v in values.iter().skip(1) {
37 if v < lo {
38 lo = v;
39 }
40 if v > hi {
41 hi = v;
42 }
43 }
44 if (hi - lo).abs() < 1e-12 {
46 hi = lo + 1.0;
47 }
48 Self { min: lo, max: hi }
49 }
50
51 #[inline]
53 pub fn encode(&self, v: f32) -> u16 {
54 let clamped = v.max(self.min).min(self.max);
55 let t = (clamped - self.min) / (self.max - self.min);
56 (t * 65535.0).round() as u16
57 }
58
59 #[inline]
61 pub fn decode(&self, q: u16) -> f32 {
62 let t = q as f32 / 65535.0;
63 self.min + t * (self.max - self.min)
64 }
65}
66
67#[derive(Debug, Clone)]
76pub struct QuantizedMesh {
77 pub positions: Vec<[u16; 3]>,
78 pub normals: Vec<[i8; 3]>,
79 pub uvs: Vec<[u16; 2]>,
80 pub indices: Vec<u32>,
81 pub pos_range: [QuantizeRange; 3],
82 pub has_suit: bool,
83}
84
85#[derive(Debug, Clone)]
89pub struct QuantizeStats {
90 pub position_error_rms: f32,
91 pub normal_error_rms: f32,
92 pub uv_error_rms: f32,
93 pub compression_ratio: f32,
95}
96
97pub fn quantize_mesh(mesh: &MeshBuffers) -> QuantizedMesh {
101 let n = mesh.positions.len();
102
103 let xs: Vec<f32> = mesh.positions.iter().map(|p| p[0]).collect();
105 let ys: Vec<f32> = mesh.positions.iter().map(|p| p[1]).collect();
106 let zs: Vec<f32> = mesh.positions.iter().map(|p| p[2]).collect();
107 let rx = QuantizeRange::from_data(&xs);
108 let ry = QuantizeRange::from_data(&ys);
109 let rz = QuantizeRange::from_data(&zs);
110
111 let us: Vec<f32> = mesh.uvs.iter().map(|uv| uv[0]).collect();
113 let vs: Vec<f32> = mesh.uvs.iter().map(|uv| uv[1]).collect();
114 let ru = QuantizeRange::from_data(&us);
115 let rv = QuantizeRange::from_data(&vs);
116
117 let mut positions = Vec::with_capacity(n);
118 let mut normals = Vec::with_capacity(n);
119 let mut uvs = Vec::with_capacity(n);
120
121 for i in 0..n {
122 let [px, py, pz] = mesh.positions[i];
123 positions.push([rx.encode(px), ry.encode(py), rz.encode(pz)]);
124
125 let [nx, ny, nz] = if i < mesh.normals.len() {
127 mesh.normals[i]
128 } else {
129 [0.0, 0.0, 1.0]
130 };
131 normals.push([
132 encode_normal_component(nx),
133 encode_normal_component(ny),
134 encode_normal_component(nz),
135 ]);
136
137 let [pu, pv] = if i < mesh.uvs.len() {
138 mesh.uvs[i]
139 } else {
140 [0.0, 0.0]
141 };
142 uvs.push([ru.encode(pu), rv.encode(pv)]);
143 }
144
145 QuantizedMesh {
146 positions,
147 normals,
148 uvs,
149 indices: mesh.indices.clone(),
150 pos_range: [rx, ry, rz],
151 has_suit: mesh.has_suit,
152 }
153}
154
155pub fn dequantize_mesh(q: &QuantizedMesh) -> MeshBuffers {
161 let n = q.positions.len();
162
163 let ru = QuantizeRange { min: 0.0, max: 1.0 };
167 let rv = QuantizeRange { min: 0.0, max: 1.0 };
168
169 let mut positions = Vec::with_capacity(n);
170 let mut normals = Vec::with_capacity(n);
171 let mut uvs = Vec::with_capacity(n);
172
173 let [ref rx, ref ry, ref rz] = q.pos_range;
174
175 for i in 0..n {
176 let [qx, qy, qz] = q.positions[i];
177 positions.push([rx.decode(qx), ry.decode(qy), rz.decode(qz)]);
178
179 let [enx, eny, enz] = q.normals[i];
180 let nx = decode_normal_component(enx);
181 let ny = decode_normal_component(eny);
182 let nz = decode_normal_component(enz);
183 let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-9);
185 normals.push([nx / len, ny / len, nz / len]);
186
187 let [qu, qv] = q.uvs[i];
188 uvs.push([ru.decode(qu), rv.decode(qv)]);
189 }
190
191 let tangents = vec![[1.0f32, 0.0, 0.0, 1.0]; n];
192
193 MeshBuffers {
194 positions,
195 normals,
196 tangents,
197 uvs,
198 indices: q.indices.clone(),
199 colors: None,
200 has_suit: q.has_suit,
201 }
202}
203
204pub fn quantize_stats(original: &MeshBuffers, q: &QuantizedMesh) -> QuantizeStats {
209 let reconstructed = dequantize_mesh(q);
210 let n = original.positions.len().min(reconstructed.positions.len());
211
212 let pos_rms = if n == 0 {
214 0.0
215 } else {
216 let sum_sq: f32 = (0..n)
217 .map(|i| {
218 let [ox, oy, oz] = original.positions[i];
219 let [rx, ry, rz] = reconstructed.positions[i];
220 let d = [(ox - rx), (oy - ry), (oz - rz)];
221 d[0] * d[0] + d[1] * d[1] + d[2] * d[2]
222 })
223 .sum();
224 (sum_sq / n as f32).sqrt()
225 };
226
227 let nor_rms = if n == 0 {
229 0.0
230 } else {
231 let mn = original
232 .normals
233 .len()
234 .min(reconstructed.normals.len())
235 .min(n);
236 let sum_sq: f32 = (0..mn)
237 .map(|i| {
238 let [ox, oy, oz] = original.normals[i];
239 let [rx, ry, rz] = reconstructed.normals[i];
240 let d = [(ox - rx), (oy - ry), (oz - rz)];
241 d[0] * d[0] + d[1] * d[1] + d[2] * d[2]
242 })
243 .sum();
244 (sum_sq / mn as f32).sqrt()
245 };
246
247 let uv_rms = if n == 0 {
249 0.0
250 } else {
251 let mu = original.uvs.len().min(reconstructed.uvs.len()).min(n);
252 let sum_sq: f32 = (0..mu)
253 .map(|i| {
254 let [ou, ov] = original.uvs[i];
255 let [ru, rv] = reconstructed.uvs[i];
256 let du = ou - ru;
257 let dv = ov - rv;
258 du * du + dv * dv
259 })
260 .sum();
261 (sum_sq / mu as f32).sqrt()
262 };
263
264 let orig_bytes = original.positions.len() * 12 + original.normals.len() * 12 + original.uvs.len() * 8 + original.indices.len() * 4; let quant_bytes = q.positions.len() * 6 + q.normals.len() * 3 + q.uvs.len() * 4 + q.indices.len() * 4; let compression_ratio = if quant_bytes == 0 {
276 1.0
277 } else {
278 orig_bytes as f32 / quant_bytes as f32
279 };
280
281 QuantizeStats {
282 position_error_rms: pos_rms,
283 normal_error_rms: nor_rms,
284 uv_error_rms: uv_rms,
285 compression_ratio,
286 }
287}
288
289pub fn encode_normal_oct(n: [f32; 3]) -> [i8; 2] {
296 let l1 = n[0].abs() + n[1].abs() + n[2].abs();
298 let (mut ox, mut oy) = if l1 < 1e-9 {
299 (0.0f32, 0.0f32)
300 } else {
301 (n[0] / l1, n[1] / l1)
302 };
303
304 if n[2] < 0.0 {
306 let ox2 = (1.0 - oy.abs()) * ox.signum();
307 let oy2 = (1.0 - ox.abs()) * oy.signum();
308 ox = ox2;
309 oy = oy2;
310 }
311
312 let ex = (ox * 127.0).round().clamp(-127.0, 127.0) as i8;
314 let ey = (oy * 127.0).round().clamp(-127.0, 127.0) as i8;
315 [ex, ey]
316}
317
318pub fn decode_normal_oct(enc: [i8; 2]) -> [f32; 3] {
320 let ox = enc[0] as f32 / 127.0;
321 let oy = enc[1] as f32 / 127.0;
322 let oz = 1.0 - ox.abs() - oy.abs();
323
324 let (fx, fy) = if oz < 0.0 {
325 (
326 (1.0 - oy.abs()) * ox.signum(),
327 (1.0 - ox.abs()) * oy.signum(),
328 )
329 } else {
330 (ox, oy)
331 };
332
333 let len = (fx * fx + fy * fy + oz * oz).sqrt().max(1e-9);
334 [fx / len, fy / len, oz / len]
335}
336
337pub fn write_quantized_bin(q: &QuantizedMesh, path: &Path) -> anyhow::Result<usize> {
356 let vc = q.positions.len() as u32;
357 let ic = q.indices.len() as u32;
358
359 let mut buf: Vec<u8> = Vec::new();
360
361 buf.extend_from_slice(QMSH_MAGIC);
363 buf.extend_from_slice(&QMSH_VERSION.to_le_bytes());
364 buf.extend_from_slice(&vc.to_le_bytes());
365 buf.extend_from_slice(&ic.to_le_bytes());
366
367 for r in &q.pos_range {
369 buf.extend_from_slice(&r.min.to_le_bytes());
370 buf.extend_from_slice(&r.max.to_le_bytes());
371 }
372
373 for p in &q.positions {
375 for &v in p {
376 buf.extend_from_slice(&v.to_le_bytes());
377 }
378 }
379
380 for n in &q.normals {
382 for &b in n {
383 buf.push(b as u8);
384 }
385 }
386
387 for uv in &q.uvs {
389 for &v in uv {
390 buf.extend_from_slice(&v.to_le_bytes());
391 }
392 }
393
394 for &idx in &q.indices {
396 buf.extend_from_slice(&idx.to_le_bytes());
397 }
398
399 buf.push(q.has_suit as u8);
401
402 let total = buf.len();
403 let mut file = std::fs::File::create(path)?;
404 file.write_all(&buf)?;
405
406 Ok(total)
407}
408
409pub fn read_quantized_bin(path: &Path) -> anyhow::Result<QuantizedMesh> {
411 let mut file = std::fs::File::open(path)?;
412 let mut buf = Vec::new();
413 file.read_to_end(&mut buf)?;
414
415 anyhow::ensure!(buf.len() >= 16, "file too short for QMSH header");
416
417 anyhow::ensure!(&buf[0..4] == QMSH_MAGIC, "invalid QMSH magic");
419
420 let version = u32::from_le_bytes(buf[4..8].try_into()?);
421 anyhow::ensure!(
422 version == QMSH_VERSION,
423 "unsupported QMSH version {version}"
424 );
425
426 let vertex_count = u32::from_le_bytes(buf[8..12].try_into()?) as usize;
427 let index_count = u32::from_le_bytes(buf[12..16].try_into()?) as usize;
428
429 let mut offset = 16usize;
430
431 anyhow::ensure!(buf.len() >= offset + 24, "file truncated at range data");
433 let mut pos_range_arr = std::array::from_fn(|_| QuantizeRange { min: 0.0, max: 1.0 });
434 for r in &mut pos_range_arr {
435 r.min = f32::from_le_bytes(buf[offset..offset + 4].try_into()?);
436 offset += 4;
437 r.max = f32::from_le_bytes(buf[offset..offset + 4].try_into()?);
438 offset += 4;
439 }
440
441 let pos_bytes = vertex_count * 6;
443 anyhow::ensure!(
444 buf.len() >= offset + pos_bytes,
445 "file truncated at positions"
446 );
447 let mut positions = Vec::with_capacity(vertex_count);
448 for _ in 0..vertex_count {
449 let x = u16::from_le_bytes(buf[offset..offset + 2].try_into()?);
450 let y = u16::from_le_bytes(buf[offset + 2..offset + 4].try_into()?);
451 let z = u16::from_le_bytes(buf[offset + 4..offset + 6].try_into()?);
452 positions.push([x, y, z]);
453 offset += 6;
454 }
455
456 let nor_bytes = vertex_count * 3;
458 anyhow::ensure!(buf.len() >= offset + nor_bytes, "file truncated at normals");
459 let mut normals = Vec::with_capacity(vertex_count);
460 for _ in 0..vertex_count {
461 let nx = buf[offset] as i8;
462 let ny = buf[offset + 1] as i8;
463 let nz = buf[offset + 2] as i8;
464 normals.push([nx, ny, nz]);
465 offset += 3;
466 }
467
468 let uv_bytes = vertex_count * 4;
470 anyhow::ensure!(buf.len() >= offset + uv_bytes, "file truncated at UVs");
471 let mut uvs = Vec::with_capacity(vertex_count);
472 for _ in 0..vertex_count {
473 let u = u16::from_le_bytes(buf[offset..offset + 2].try_into()?);
474 let v = u16::from_le_bytes(buf[offset + 2..offset + 4].try_into()?);
475 uvs.push([u, v]);
476 offset += 4;
477 }
478
479 let idx_bytes = index_count * 4;
481 anyhow::ensure!(buf.len() >= offset + idx_bytes, "file truncated at indices");
482 let mut indices = Vec::with_capacity(index_count);
483 for _ in 0..index_count {
484 let idx = u32::from_le_bytes(buf[offset..offset + 4].try_into()?);
485 indices.push(idx);
486 offset += 4;
487 }
488
489 let has_suit = if offset < buf.len() {
491 buf[offset] != 0
492 } else {
493 false
494 };
495
496 Ok(QuantizedMesh {
497 positions,
498 normals,
499 uvs,
500 indices,
501 pos_range: pos_range_arr,
502 has_suit,
503 })
504}
505
506#[inline]
510fn encode_normal_component(v: f32) -> i8 {
511 (v.clamp(-1.0, 1.0) * 127.0).round() as i8
512}
513
514#[inline]
516fn decode_normal_component(b: i8) -> f32 {
517 b as f32 / 127.0
518}
519
520#[cfg(test)]
523mod tests {
524 use super::*;
525 use oxihuman_mesh::MeshBuffers;
526 use oxihuman_morph::engine::MeshBuffers as MB;
527
528 fn make_mesh(
531 positions: Vec<[f32; 3]>,
532 normals: Vec<[f32; 3]>,
533 uvs: Vec<[f32; 2]>,
534 indices: Vec<u32>,
535 ) -> MeshBuffers {
536 MeshBuffers::from_morph(MB {
537 positions,
538 normals,
539 uvs,
540 indices,
541 has_suit: false,
542 })
543 }
544
545 fn simple_triangle() -> MeshBuffers {
546 make_mesh(
547 vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]],
548 vec![[0.0, 0.0, 1.0]; 3],
549 vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]],
550 vec![0, 1, 2],
551 )
552 }
553
554 fn empty_mesh() -> MeshBuffers {
555 make_mesh(vec![], vec![], vec![], vec![])
556 }
557
558 #[test]
561 fn qrange_encode_min_gives_zero() {
562 let r = QuantizeRange {
563 min: -1.0,
564 max: 1.0,
565 };
566 assert_eq!(r.encode(-1.0), 0);
567 }
568
569 #[test]
570 fn qrange_encode_max_gives_65535() {
571 let r = QuantizeRange {
572 min: -1.0,
573 max: 1.0,
574 };
575 assert_eq!(r.encode(1.0), 65535);
576 }
577
578 #[test]
579 fn qrange_decode_zero_gives_min() {
580 let r = QuantizeRange { min: 2.0, max: 5.0 };
581 assert!((r.decode(0) - 2.0).abs() < 1e-5);
582 }
583
584 #[test]
585 fn qrange_decode_max_gives_max() {
586 let r = QuantizeRange { min: 2.0, max: 5.0 };
587 assert!((r.decode(65535) - 5.0).abs() < 1e-4);
588 }
589
590 #[test]
591 fn qrange_roundtrip_midpoint() {
592 let r = QuantizeRange { min: 0.0, max: 1.0 };
593 let original = 0.5f32;
594 let decoded = r.decode(r.encode(original));
595 assert!(
596 (decoded - original).abs() < 1.0 / 65535.0 * 2.0,
597 "roundtrip error {} too large",
598 (decoded - original).abs()
599 );
600 }
601
602 #[test]
603 fn qrange_from_data_detects_extremes() {
604 let data = vec![-3.0f32, 0.0, 7.5, 2.1];
605 let r = QuantizeRange::from_data(&data);
606 assert!((r.min - (-3.0)).abs() < 1e-6);
607 assert!((r.max - 7.5).abs() < 1e-6);
608 }
609
610 #[test]
611 fn qrange_from_empty_is_valid() {
612 let r = QuantizeRange::from_data(&[]);
613 assert!((r.max - r.min).abs() > 1e-9);
615 }
616
617 #[test]
618 fn qrange_clamps_out_of_range_value() {
619 let r = QuantizeRange { min: 0.0, max: 1.0 };
620 assert_eq!(r.encode(2.0), 65535);
621 assert_eq!(r.encode(-1.0), 0);
622 }
623
624 #[test]
627 fn quantize_vertex_count_preserved() {
628 let mesh = simple_triangle();
629 let q = quantize_mesh(&mesh);
630 assert_eq!(q.positions.len(), 3);
631 assert_eq!(q.normals.len(), 3);
632 assert_eq!(q.uvs.len(), 3);
633 }
634
635 #[test]
636 fn quantize_index_count_preserved() {
637 let mesh = simple_triangle();
638 let q = quantize_mesh(&mesh);
639 assert_eq!(q.indices, vec![0, 1, 2]);
640 }
641
642 #[test]
643 fn dequantize_roundtrip_position_error_small() {
644 let mesh = simple_triangle();
645 let q = quantize_mesh(&mesh);
646 let rec = dequantize_mesh(&q);
647 for (orig, recon) in mesh.positions.iter().zip(rec.positions.iter()) {
648 let err = (0..3)
649 .map(|i| (orig[i] - recon[i]).abs())
650 .fold(0.0f32, f32::max);
651 assert!(err < 1e-3, "position error {err} too large");
652 }
653 }
654
655 #[test]
656 fn dequantize_normal_unit_length() {
657 let mesh = simple_triangle();
658 let q = quantize_mesh(&mesh);
659 let rec = dequantize_mesh(&q);
660 for n in &rec.normals {
661 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
662 assert!((len - 1.0).abs() < 0.01, "normal not unit-length: {len}");
663 }
664 }
665
666 #[test]
667 fn quantize_empty_mesh_no_panic() {
668 let mesh = empty_mesh();
669 let q = quantize_mesh(&mesh);
670 assert!(q.positions.is_empty());
671 assert!(q.indices.is_empty());
672 }
673
674 #[test]
675 fn dequantize_empty_mesh_no_panic() {
676 let mesh = empty_mesh();
677 let q = quantize_mesh(&mesh);
678 let rec = dequantize_mesh(&q);
679 assert!(rec.positions.is_empty());
680 }
681
682 #[test]
685 fn stats_compression_ratio_above_one() {
686 let positions: Vec<[f32; 3]> = (0..100).map(|i| [i as f32 * 0.01, 0.0, 0.0]).collect();
688 let normals = vec![[0.0f32, 0.0, 1.0]; 100];
689 let uvs: Vec<[f32; 2]> = (0..100).map(|i| [i as f32 * 0.01, 0.0]).collect();
690 let indices: Vec<u32> = (0..99).flat_map(|i| [i, i + 1, i]).collect();
691 let mesh = make_mesh(positions, normals, uvs, indices);
692 let q = quantize_mesh(&mesh);
693 let stats = quantize_stats(&mesh, &q);
694 assert!(
695 stats.compression_ratio > 1.0,
696 "expected compression_ratio > 1, got {}",
697 stats.compression_ratio
698 );
699 }
700
701 #[test]
702 fn stats_position_error_rms_nonnegative() {
703 let mesh = simple_triangle();
704 let q = quantize_mesh(&mesh);
705 let stats = quantize_stats(&mesh, &q);
706 assert!(stats.position_error_rms >= 0.0);
707 assert!(stats.position_error_rms < 0.01);
708 }
709
710 #[test]
711 fn stats_empty_mesh_no_panic() {
712 let mesh = empty_mesh();
713 let q = quantize_mesh(&mesh);
714 let stats = quantize_stats(&mesh, &q);
715 assert_eq!(stats.position_error_rms, 0.0);
716 assert_eq!(stats.compression_ratio, 1.0);
717 }
718
719 #[test]
722 fn oct_encode_decode_z_up() {
723 let n = [0.0f32, 0.0, 1.0];
724 let enc = encode_normal_oct(n);
725 let dec = decode_normal_oct(enc);
726 let dot = n[0] * dec[0] + n[1] * dec[1] + n[2] * dec[2];
727 assert!(dot > 0.99, "z-up oct roundtrip dot={dot}");
728 }
729
730 #[test]
731 fn oct_encode_decode_z_down() {
732 let n = [0.0f32, 0.0, -1.0];
733 let enc = encode_normal_oct(n);
734 let dec = decode_normal_oct(enc);
735 let dot = n[0] * dec[0] + n[1] * dec[1] + n[2] * dec[2];
736 assert!(dot > 0.99, "z-down oct roundtrip dot={dot}");
737 }
738
739 #[test]
740 fn oct_encode_decode_diagonal() {
741 let s = 1.0f32 / 3.0f32.sqrt();
742 let n = [s, s, s];
743 let enc = encode_normal_oct(n);
744 let dec = decode_normal_oct(enc);
745 let dot = n[0] * dec[0] + n[1] * dec[1] + n[2] * dec[2];
746 assert!(dot > 0.99, "diagonal oct roundtrip dot={dot}");
747 }
748
749 #[test]
750 fn oct_decoded_is_unit_length() {
751 for enc in [[10i8, 20], [-10, 50], [127, 0], [0, -127]] {
752 let dec = decode_normal_oct(enc);
753 let len = (dec[0] * dec[0] + dec[1] * dec[1] + dec[2] * dec[2]).sqrt();
754 assert!(
755 (len - 1.0).abs() < 0.01,
756 "oct decoded not unit-length: {len}"
757 );
758 }
759 }
760
761 #[test]
764 fn write_read_roundtrip() {
765 let mesh = simple_triangle();
766 let q = quantize_mesh(&mesh);
767 let path = std::path::Path::new("/tmp/test_qmsh_roundtrip.bin");
768 let written = write_quantized_bin(&q, path).expect("write failed");
769 assert!(
770 written > 16,
771 "expected more than header bytes, got {written}"
772 );
773
774 let q2 = read_quantized_bin(path).expect("read failed");
775 assert_eq!(q2.positions.len(), q.positions.len());
776 assert_eq!(q2.normals.len(), q.normals.len());
777 assert_eq!(q2.uvs.len(), q.uvs.len());
778 assert_eq!(q2.indices, q.indices);
779 assert_eq!(q2.positions[0], q.positions[0]);
781 }
782
783 #[test]
784 fn write_read_preserves_ranges() {
785 let mesh = simple_triangle();
786 let q = quantize_mesh(&mesh);
787 let path = std::path::Path::new("/tmp/test_qmsh_ranges.bin");
788 write_quantized_bin(&q, path).expect("write failed");
789 let q2 = read_quantized_bin(path).expect("read failed");
790 for i in 0..3 {
791 assert!((q2.pos_range[i].min - q.pos_range[i].min).abs() < 1e-5);
792 assert!((q2.pos_range[i].max - q.pos_range[i].max).abs() < 1e-5);
793 }
794 }
795
796 #[test]
797 fn write_read_empty_mesh() {
798 let mesh = empty_mesh();
799 let q = quantize_mesh(&mesh);
800 let path = std::path::Path::new("/tmp/test_qmsh_empty.bin");
801 write_quantized_bin(&q, path).expect("write failed");
802 let q2 = read_quantized_bin(path).expect("read failed");
803 assert!(q2.positions.is_empty());
804 assert!(q2.indices.is_empty());
805 }
806
807 #[test]
808 fn read_bad_magic_returns_error() {
809 let path = std::path::Path::new("/tmp/test_qmsh_badmagic.bin");
810 std::fs::write(
811 path,
812 b"BAAD\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
813 )
814 .expect("should succeed");
815 assert!(read_quantized_bin(path).is_err());
816 }
817}