1use anyhow::{anyhow, ensure, Context, Result};
42use serde::{Deserialize, Serialize};
43
44pub const OHPK_MAGIC: [u8; 4] = *b"OHPK";
48
49pub const OHPK_VERSION: u8 = 1;
51
52pub const OHPK_FLAG_DEFLATE: u8 = 0x01;
54
55pub const OHPK_DEFLATE_LEVEL: u8 = 9;
57
58pub const MODEL_UNIT_MM: f32 = 100.0;
64
65const I16_STEPS: f32 = 65_535.0;
68
69const I16_OFFSET: f32 = 32_768.0;
72
73const I16_HALF: f32 = 32_767.0;
75
76#[inline]
78pub fn model_units_to_mm(units: f32) -> f32 {
79 units * MODEL_UNIT_MM
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct CorePackFile {
87 pub name: String,
89 pub sha256: String,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct CorePackProvenance {
96 pub upstream_repo: String,
98 pub upstream_commit: String,
100 #[serde(default)]
102 pub files: Vec<CorePackFile>,
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct CorePackManifest {
108 pub name: String,
110 pub version: String,
112 #[serde(default = "default_license")]
114 pub license: String,
115 pub provenance: CorePackProvenance,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub age_floor_years: Option<f32>,
120 #[serde(default)]
122 pub categories: Vec<String>,
123 #[serde(default)]
125 pub target_names: Vec<String>,
126}
127
128fn default_license() -> String {
129 "CC0-1.0".to_string()
130}
131
132impl CorePackManifest {
133 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
135 Self {
136 name: name.into(),
137 version: version.into(),
138 license: default_license(),
139 provenance: CorePackProvenance {
140 upstream_repo: String::new(),
141 upstream_commit: String::new(),
142 files: Vec::new(),
143 },
144 age_floor_years: None,
145 categories: Vec::new(),
146 target_names: Vec::new(),
147 }
148 }
149}
150
151#[derive(Debug, Clone, PartialEq)]
155pub struct TargetErrorReport {
156 pub name: String,
158 pub max_error_units: f32,
160 pub max_error_mm: f32,
162}
163
164#[derive(Debug, Clone, PartialEq)]
166pub struct QuantizationReport {
167 pub base_pos_max_error_units: f32,
169 pub base_pos_max_error_mm: f32,
171 pub base_uv_max_error: Option<f32>,
173 pub targets: Vec<TargetErrorReport>,
175}
176
177#[inline]
180fn push_u16(buf: &mut Vec<u8>, v: u16) {
181 buf.extend_from_slice(&v.to_le_bytes());
182}
183
184#[inline]
185fn push_u32(buf: &mut Vec<u8>, v: u32) {
186 buf.extend_from_slice(&v.to_le_bytes());
187}
188
189#[inline]
190fn push_i16(buf: &mut Vec<u8>, v: i16) {
191 buf.extend_from_slice(&v.to_le_bytes());
192}
193
194#[inline]
195fn push_f32(buf: &mut Vec<u8>, v: f32) {
196 buf.extend_from_slice(&v.to_le_bytes());
197}
198
199fn push_uvarint(buf: &mut Vec<u8>, mut v: u64) {
201 loop {
202 let byte = (v & 0x7f) as u8;
203 v >>= 7;
204 if v == 0 {
205 buf.push(byte);
206 return;
207 }
208 buf.push(byte | 0x80);
209 }
210}
211
212struct Reader<'a> {
216 data: &'a [u8],
217 pos: usize,
218}
219
220impl<'a> Reader<'a> {
221 fn new(data: &'a [u8]) -> Self {
222 Self { data, pos: 0 }
223 }
224
225 #[inline]
226 fn remaining(&self) -> usize {
227 self.data.len().saturating_sub(self.pos)
228 }
229
230 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
231 let end = self
232 .pos
233 .checked_add(n)
234 .ok_or_else(|| anyhow!("OHPK reader length overflow"))?;
235 ensure!(
236 end <= self.data.len(),
237 "OHPK truncated: need {n} bytes at offset {} but only {} remain",
238 self.pos,
239 self.remaining()
240 );
241 let slice = &self.data[self.pos..end];
242 self.pos = end;
243 Ok(slice)
244 }
245
246 fn u8(&mut self) -> Result<u8> {
247 Ok(self.take(1)?[0])
248 }
249
250 fn u16(&mut self) -> Result<u16> {
251 let b: [u8; 2] = self
252 .take(2)?
253 .try_into()
254 .map_err(|_| anyhow!("OHPK u16 slice conversion"))?;
255 Ok(u16::from_le_bytes(b))
256 }
257
258 fn u32(&mut self) -> Result<u32> {
259 let b: [u8; 4] = self
260 .take(4)?
261 .try_into()
262 .map_err(|_| anyhow!("OHPK u32 slice conversion"))?;
263 Ok(u32::from_le_bytes(b))
264 }
265
266 fn i16(&mut self) -> Result<i16> {
267 let b: [u8; 2] = self
268 .take(2)?
269 .try_into()
270 .map_err(|_| anyhow!("OHPK i16 slice conversion"))?;
271 Ok(i16::from_le_bytes(b))
272 }
273
274 fn f32(&mut self) -> Result<f32> {
275 let b: [u8; 4] = self
276 .take(4)?
277 .try_into()
278 .map_err(|_| anyhow!("OHPK f32 slice conversion"))?;
279 Ok(f32::from_le_bytes(b))
280 }
281
282 fn len_prefixed_u32(&mut self) -> Result<&'a [u8]> {
284 let n = self.u32()? as usize;
285 self.take(n)
286 }
287
288 fn string_u16(&mut self) -> Result<String> {
290 let n = self.u16()? as usize;
291 let bytes = self.take(n)?;
292 String::from_utf8(bytes.to_vec()).context("OHPK invalid UTF-8 string")
293 }
294
295 fn uvarint(&mut self) -> Result<u64> {
297 let mut result: u64 = 0;
298 let mut shift: u32 = 0;
299 loop {
300 let byte = self.u8()?;
301 ensure!(shift < 64, "OHPK varint overflow");
302 result |= u64::from(byte & 0x7f) << shift;
303 if byte & 0x80 == 0 {
304 return Ok(result);
305 }
306 shift += 7;
307 }
308 }
309}
310
311#[derive(Debug, Clone, Copy)]
315struct AxisQuant {
316 scale: f32,
317 bias: f32,
318}
319
320impl AxisQuant {
321 fn fit(lo: f32, hi: f32) -> Self {
323 let span = hi - lo;
324 if span > 0.0 && span.is_finite() {
325 let scale = span / I16_STEPS;
326 let bias = lo + I16_OFFSET * scale;
328 Self { scale, bias }
329 } else {
330 Self {
332 scale: 0.0,
333 bias: lo,
334 }
335 }
336 }
337
338 #[inline]
339 fn quantise(&self, v: f32) -> i16 {
340 if self.scale > 0.0 {
341 let t = ((v - self.bias) / self.scale).round();
342 t.clamp(-I16_OFFSET, I16_HALF) as i16
343 } else {
344 0
345 }
346 }
347
348 #[inline]
349 fn dequantise(&self, q: i16) -> f32 {
350 f32::from(q) * self.scale + self.bias
351 }
352}
353
354fn fit_axes(data: &[f32], stride: usize, dims: usize) -> Vec<AxisQuant> {
359 let mut lo = vec![f32::INFINITY; dims];
360 let mut hi = vec![f32::NEG_INFINITY; dims];
361 for chunk in data.chunks_exact(stride) {
362 for (axis, slot) in chunk.iter().take(dims).enumerate() {
363 let v = *slot;
364 if v < lo[axis] {
365 lo[axis] = v;
366 }
367 if v > hi[axis] {
368 hi[axis] = v;
369 }
370 }
371 }
372 (0..dims)
373 .map(|axis| {
374 let (l, h) = if lo[axis].is_finite() && hi[axis].is_finite() {
375 (lo[axis], hi[axis])
376 } else {
377 (0.0, 0.0)
378 };
379 AxisQuant::fit(l, h)
380 })
381 .collect()
382}
383
384struct BuilderTarget {
387 name: String,
388 category: String,
389 sparse: Vec<(u32, [f32; 3])>,
390}
391
392pub struct CorePackBuilder {
400 manifest: Option<CorePackManifest>,
401 base_positions: Vec<f32>,
402 base_indices: Vec<u32>,
403 base_uvs: Option<Vec<f32>>,
404 helper_meta: Option<Vec<u8>>,
405 targets: Vec<BuilderTarget>,
406}
407
408impl Default for CorePackBuilder {
409 fn default() -> Self {
410 Self::new()
411 }
412}
413
414impl CorePackBuilder {
415 pub fn new() -> Self {
417 Self {
418 manifest: None,
419 base_positions: Vec::new(),
420 base_indices: Vec::new(),
421 base_uvs: None,
422 helper_meta: None,
423 targets: Vec::new(),
424 }
425 }
426
427 pub fn set_base_mesh(
436 &mut self,
437 positions: &[f32],
438 indices: &[u32],
439 uvs: Option<&[f32]>,
440 ) -> &mut Self {
441 self.base_positions = positions.to_vec();
442 self.base_indices = indices.to_vec();
443 self.base_uvs = uvs.map(<[f32]>::to_vec);
444 self
445 }
446
447 pub fn set_helper_metadata(&mut self, bytes: &[u8]) -> &mut Self {
449 self.helper_meta = Some(bytes.to_vec());
450 self
451 }
452
453 pub fn add_target(
459 &mut self,
460 name: impl Into<String>,
461 category: impl Into<String>,
462 sparse: &[(u32, [f32; 3])],
463 ) -> &mut Self {
464 self.targets.push(BuilderTarget {
465 name: name.into(),
466 category: category.into(),
467 sparse: sparse.to_vec(),
468 });
469 self
470 }
471
472 pub fn set_manifest(&mut self, manifest: CorePackManifest) -> &mut Self {
474 self.manifest = Some(manifest);
475 self
476 }
477
478 pub fn build(&self) -> Result<Vec<u8>> {
483 ensure!(
484 !self.base_positions.is_empty(),
485 "cannot build an empty core pack: no base mesh set"
486 );
487 ensure!(
488 self.base_positions.len().is_multiple_of(3),
489 "base positions length {} is not a multiple of 3",
490 self.base_positions.len()
491 );
492 let n_verts = self.base_positions.len() / 3;
493 ensure!(n_verts > 0, "cannot build a core pack with zero vertices");
494
495 if let Some(uvs) = &self.base_uvs {
496 ensure!(
497 uvs.len() == n_verts * 2,
498 "UV buffer length {} does not match 2 * n_verts ({})",
499 uvs.len(),
500 n_verts * 2
501 );
502 }
503
504 let manifest = self.resolved_manifest();
505 let manifest_json =
506 serde_json::to_vec(&manifest).context("serialising OHPK manifest to JSON")?;
507
508 let mut body = Vec::new();
510 push_u32(&mut body, u32::try_from(manifest_json.len()).context("manifest too large")?);
511 body.extend_from_slice(&manifest_json);
512
513 self.encode_base_mesh(&mut body, n_verts)?;
514
515 push_u32(
516 &mut body,
517 u32::try_from(self.targets.len()).context("too many targets")?,
518 );
519 for target in &self.targets {
520 encode_target(&mut body, target);
521 }
522
523 let uncompressed_len = u32::try_from(body.len()).context("OHPK body exceeds 4 GiB")?;
525 let compressed = oxiarc_deflate::deflate(&body, OHPK_DEFLATE_LEVEL)
526 .map_err(|e| anyhow!("OHPK deflate failed: {e}"))?;
527
528 let mut out = Vec::with_capacity(10 + compressed.len());
529 out.extend_from_slice(&OHPK_MAGIC);
530 out.push(OHPK_VERSION);
531 out.push(OHPK_FLAG_DEFLATE);
532 push_u32(&mut out, uncompressed_len);
533 out.extend_from_slice(&compressed);
534 Ok(out)
535 }
536
537 fn resolved_manifest(&self) -> CorePackManifest {
540 let mut manifest = self
541 .manifest
542 .clone()
543 .unwrap_or_else(|| CorePackManifest::new("oxihuman-core", "0"));
544 if manifest.target_names.is_empty() {
545 manifest.target_names = self.targets.iter().map(|t| t.name.clone()).collect();
546 }
547 manifest
548 }
549
550 fn encode_base_mesh(&self, body: &mut Vec<u8>, n_verts: usize) -> Result<()> {
551 let pos_axes = fit_axes(&self.base_positions, 3, 3);
552 let n_indices = self.base_indices.len();
553
554 push_u32(body, u32::try_from(n_verts).context("n_verts too large")?);
555 push_u32(body, u32::try_from(n_indices).context("n_indices too large")?);
556
557 for a in &pos_axes {
558 push_f32(body, a.scale);
559 }
560 for a in &pos_axes {
561 push_f32(body, a.bias);
562 }
563
564 let mut pos_max_err = 0.0f32;
566 let mut quantised: Vec<i16> = Vec::with_capacity(n_verts * 3);
567 for chunk in self.base_positions.chunks_exact(3) {
568 for (axis, value) in chunk.iter().enumerate() {
569 let q = pos_axes[axis].quantise(*value);
570 let err = (value - pos_axes[axis].dequantise(q)).abs();
571 if err > pos_max_err {
572 pos_max_err = err;
573 }
574 quantised.push(q);
575 }
576 }
577 push_f32(body, pos_max_err);
578 for q in &quantised {
579 push_i16(body, *q);
580 }
581
582 for idx in &self.base_indices {
583 push_u32(body, *idx);
584 }
585
586 match &self.base_uvs {
588 Some(uvs) => {
589 body.push(1u8);
590 let uv_axes = fit_axes(uvs, 2, 2);
591 for a in &uv_axes {
592 push_f32(body, a.scale);
593 }
594 for a in &uv_axes {
595 push_f32(body, a.bias);
596 }
597 let mut uv_max_err = 0.0f32;
598 let mut uv_q: Vec<i16> = Vec::with_capacity(n_verts * 2);
599 for pair in uvs.chunks_exact(2) {
600 for (axis, value) in pair.iter().enumerate() {
601 let q = uv_axes[axis].quantise(*value);
602 let err = (value - uv_axes[axis].dequantise(q)).abs();
603 if err > uv_max_err {
604 uv_max_err = err;
605 }
606 uv_q.push(q);
607 }
608 }
609 push_f32(body, uv_max_err);
610 for q in &uv_q {
611 push_i16(body, *q);
612 }
613 }
614 None => body.push(0u8),
615 }
616
617 match &self.helper_meta {
619 Some(meta) => {
620 body.push(1u8);
621 push_u32(body, u32::try_from(meta.len()).context("helper meta too large")?);
622 body.extend_from_slice(meta);
623 }
624 None => body.push(0u8),
625 }
626
627 Ok(())
628 }
629}
630
631fn encode_target(body: &mut Vec<u8>, target: &BuilderTarget) {
633 let mut sparse = target.sparse.clone();
635 sparse.sort_by_key(|(idx, _)| *idx);
636
637 let max_abs = sparse
639 .iter()
640 .flat_map(|(_, d)| d.iter())
641 .fold(0.0f32, |acc, &c| acc.max(c.abs()));
642 let scale = if max_abs > 0.0 && max_abs.is_finite() {
643 max_abs / I16_HALF
644 } else {
645 0.0
646 };
647
648 let mut quantised: Vec<[i16; 3]> = Vec::with_capacity(sparse.len());
650 let mut max_err = 0.0f32;
651 for (_, d) in &sparse {
652 let mut q = [0i16; 3];
653 for axis in 0..3 {
654 let qi = if scale > 0.0 {
655 (d[axis] / scale).round().clamp(-I16_HALF, I16_HALF) as i16
656 } else {
657 0
658 };
659 let deq = f32::from(qi) * scale;
660 let err = (d[axis] - deq).abs();
661 if err > max_err {
662 max_err = err;
663 }
664 q[axis] = qi;
665 }
666 quantised.push(q);
667 }
668
669 let name = target.name.as_bytes();
670 let category = target.category.as_bytes();
671 push_u16(body, name.len() as u16);
672 body.extend_from_slice(name);
673 push_u16(body, category.len() as u16);
674 body.extend_from_slice(category);
675
676 push_f32(body, scale);
677 push_f32(body, max_err);
678 push_u32(body, sparse.len() as u32);
679
680 let mut prev = 0u32;
682 for (idx, _) in &sparse {
683 let delta = idx.wrapping_sub(prev);
684 push_uvarint(body, u64::from(delta));
685 prev = *idx;
686 }
687
688 for q in &quantised {
690 push_i16(body, q[0]);
691 push_i16(body, q[1]);
692 push_i16(body, q[2]);
693 }
694}
695
696struct BaseMesh {
700 n_verts: usize,
701 pos_axes: [AxisQuant; 3],
702 pos_max_err: f32,
703 positions_q: Vec<i16>,
704 indices: Vec<u32>,
705 uvs: Option<UvSection>,
706 helper_meta: Option<Vec<u8>>,
707}
708
709struct UvSection {
710 axes: [AxisQuant; 2],
711 max_err: f32,
712 data_q: Vec<i16>,
713}
714
715pub struct CorePackTarget {
717 name: String,
718 category: String,
719 scale: f32,
720 max_abs_error_units: f32,
721 indices: Vec<u32>,
722 deltas_q: Vec<[i16; 3]>,
723}
724
725impl CorePackTarget {
726 #[inline]
728 pub fn name(&self) -> &str {
729 &self.name
730 }
731
732 #[inline]
734 pub fn category(&self) -> &str {
735 &self.category
736 }
737
738 #[inline]
740 pub fn len(&self) -> usize {
741 self.indices.len()
742 }
743
744 #[inline]
746 pub fn is_empty(&self) -> bool {
747 self.indices.is_empty()
748 }
749
750 #[inline]
752 pub fn max_abs_error_units(&self) -> f32 {
753 self.max_abs_error_units
754 }
755
756 #[inline]
758 pub fn indices(&self) -> &[u32] {
759 &self.indices
760 }
761
762 pub fn sparse(&self) -> Vec<(u32, [f32; 3])> {
764 self.indices
765 .iter()
766 .zip(&self.deltas_q)
767 .map(|(&idx, q)| {
768 (
769 idx,
770 [
771 f32::from(q[0]) * self.scale,
772 f32::from(q[1]) * self.scale,
773 f32::from(q[2]) * self.scale,
774 ],
775 )
776 })
777 .collect()
778 }
779}
780
781pub struct CorePack {
783 manifest: CorePackManifest,
784 base: BaseMesh,
785 targets: Vec<CorePackTarget>,
786}
787
788impl CorePack {
789 pub fn parse(bytes: &[u8]) -> Result<Self> {
795 ensure!(bytes.len() >= 10, "OHPK too short for a header");
796 ensure!(bytes[0..4] == OHPK_MAGIC, "bad OHPK magic bytes");
797 let version = bytes[4];
798 ensure!(
799 version == OHPK_VERSION,
800 "unsupported OHPK version {version} (expected {OHPK_VERSION})"
801 );
802 let flags = bytes[5];
803 let declared_len = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]) as usize;
804 let payload = &bytes[10..];
805
806 let body = if flags & OHPK_FLAG_DEFLATE != 0 {
807 let inflated =
808 oxiarc_deflate::inflate(payload).map_err(|e| anyhow!("OHPK inflate failed: {e}"))?;
809 ensure!(
810 inflated.len() == declared_len,
811 "OHPK body length mismatch: header says {declared_len}, inflated {}",
812 inflated.len()
813 );
814 inflated
815 } else {
816 ensure!(
817 payload.len() == declared_len,
818 "OHPK body length mismatch: header says {declared_len}, payload {}",
819 payload.len()
820 );
821 payload.to_vec()
822 };
823
824 Self::parse_body(&body)
825 }
826
827 fn parse_body(body: &[u8]) -> Result<Self> {
828 let mut r = Reader::new(body);
829
830 let manifest_bytes = r.len_prefixed_u32().context("reading manifest JSON")?;
831 let manifest: CorePackManifest =
832 serde_json::from_slice(manifest_bytes).context("parsing OHPK manifest JSON")?;
833
834 let base = parse_base_mesh(&mut r)?;
835
836 let target_count = r.u32().context("reading target count")? as usize;
837 let mut targets = Vec::with_capacity(target_count.min(1024));
838 for i in 0..target_count {
839 let t = parse_target(&mut r).with_context(|| format!("parsing target #{i}"))?;
840 targets.push(t);
841 }
842
843 Ok(Self {
844 manifest,
845 base,
846 targets,
847 })
848 }
849
850 #[inline]
852 pub fn manifest(&self) -> &CorePackManifest {
853 &self.manifest
854 }
855
856 #[inline]
858 pub fn vertex_count(&self) -> usize {
859 self.base.n_verts
860 }
861
862 pub fn base_positions(&self) -> Vec<f32> {
864 let mut out = Vec::with_capacity(self.base.positions_q.len());
865 for chunk in self.base.positions_q.chunks_exact(3) {
866 for (axis, &q) in chunk.iter().enumerate() {
867 out.push(self.base.pos_axes[axis].dequantise(q));
868 }
869 }
870 out
871 }
872
873 #[inline]
875 pub fn base_indices(&self) -> &[u32] {
876 &self.base.indices
877 }
878
879 pub fn base_uvs(&self) -> Option<Vec<f32>> {
881 self.base.uvs.as_ref().map(|uv| {
882 let mut out = Vec::with_capacity(uv.data_q.len());
883 for pair in uv.data_q.chunks_exact(2) {
884 for (axis, &q) in pair.iter().enumerate() {
885 out.push(uv.axes[axis].dequantise(q));
886 }
887 }
888 out
889 })
890 }
891
892 #[inline]
894 pub fn helper_metadata(&self) -> Option<&[u8]> {
895 self.base.helper_meta.as_deref()
896 }
897
898 #[inline]
900 pub fn targets(&self) -> &[CorePackTarget] {
901 &self.targets
902 }
903
904 pub fn quantization_report(&self) -> QuantizationReport {
906 let targets = self
907 .targets
908 .iter()
909 .map(|t| TargetErrorReport {
910 name: t.name.clone(),
911 max_error_units: t.max_abs_error_units,
912 max_error_mm: model_units_to_mm(t.max_abs_error_units),
913 })
914 .collect();
915 QuantizationReport {
916 base_pos_max_error_units: self.base.pos_max_err,
917 base_pos_max_error_mm: model_units_to_mm(self.base.pos_max_err),
918 base_uv_max_error: self.base.uvs.as_ref().map(|uv| uv.max_err),
919 targets,
920 }
921 }
922}
923
924fn parse_base_mesh(r: &mut Reader<'_>) -> Result<BaseMesh> {
925 let n_verts = r.u32().context("reading n_verts")? as usize;
926 let n_indices = r.u32().context("reading n_indices")? as usize;
927
928 let pos_axes = [
929 AxisQuant {
930 scale: r.f32()?,
931 bias: 0.0,
932 },
933 AxisQuant {
934 scale: r.f32()?,
935 bias: 0.0,
936 },
937 AxisQuant {
938 scale: r.f32()?,
939 bias: 0.0,
940 },
941 ];
942 let biases = [r.f32()?, r.f32()?, r.f32()?];
944 let pos_axes = [
945 AxisQuant {
946 scale: pos_axes[0].scale,
947 bias: biases[0],
948 },
949 AxisQuant {
950 scale: pos_axes[1].scale,
951 bias: biases[1],
952 },
953 AxisQuant {
954 scale: pos_axes[2].scale,
955 bias: biases[2],
956 },
957 ];
958
959 let pos_max_err = r.f32().context("reading base position error")?;
960
961 let mut positions_q = Vec::with_capacity(n_verts.min(1 << 20) * 3);
962 for _ in 0..n_verts * 3 {
963 positions_q.push(r.i16()?);
964 }
965
966 let mut indices = Vec::with_capacity(n_indices.min(1 << 21));
967 for _ in 0..n_indices {
968 indices.push(r.u32()?);
969 }
970
971 let has_uvs = r.u8().context("reading has_uvs flag")?;
973 let uvs = if has_uvs != 0 {
974 let axes = [
975 AxisQuant {
976 scale: r.f32()?,
977 bias: 0.0,
978 },
979 AxisQuant {
980 scale: r.f32()?,
981 bias: 0.0,
982 },
983 ];
984 let uv_bias = [r.f32()?, r.f32()?];
985 let axes = [
986 AxisQuant {
987 scale: axes[0].scale,
988 bias: uv_bias[0],
989 },
990 AxisQuant {
991 scale: axes[1].scale,
992 bias: uv_bias[1],
993 },
994 ];
995 let max_err = r.f32()?;
996 let mut data_q = Vec::with_capacity(n_verts.min(1 << 20) * 2);
997 for _ in 0..n_verts * 2 {
998 data_q.push(r.i16()?);
999 }
1000 Some(UvSection {
1001 axes,
1002 max_err,
1003 data_q,
1004 })
1005 } else {
1006 None
1007 };
1008
1009 let has_helper = r.u8().context("reading has_helper flag")?;
1011 let helper_meta = if has_helper != 0 {
1012 Some(r.len_prefixed_u32().context("reading helper meta")?.to_vec())
1013 } else {
1014 None
1015 };
1016
1017 Ok(BaseMesh {
1018 n_verts,
1019 pos_axes,
1020 pos_max_err,
1021 positions_q,
1022 indices,
1023 uvs,
1024 helper_meta,
1025 })
1026}
1027
1028fn parse_target(r: &mut Reader<'_>) -> Result<CorePackTarget> {
1029 let name = r.string_u16().context("reading target name")?;
1030 let category = r.string_u16().context("reading target category")?;
1031 let scale = r.f32().context("reading target scale")?;
1032 let max_abs_error_units = r.f32().context("reading target error")?;
1033 let n = r.u32().context("reading target affected count")? as usize;
1034
1035 let mut indices = Vec::with_capacity(n.min(1 << 20));
1036 let mut prev = 0u32;
1037 for _ in 0..n {
1038 let delta = u32::try_from(r.uvarint()?).context("target index delta overflow")?;
1039 let idx = prev.wrapping_add(delta);
1040 indices.push(idx);
1041 prev = idx;
1042 }
1043
1044 let mut deltas_q = Vec::with_capacity(n.min(1 << 20));
1045 for _ in 0..n {
1046 deltas_q.push([r.i16()?, r.i16()?, r.i16()?]);
1047 }
1048
1049 Ok(CorePackTarget {
1050 name,
1051 category,
1052 scale,
1053 max_abs_error_units,
1054 indices,
1055 deltas_q,
1056 })
1057}
1058
1059#[cfg(test)]
1062mod tests {
1063 use super::*;
1064
1065 fn sample_manifest() -> CorePackManifest {
1066 CorePackManifest {
1067 name: "oxihuman-core".to_string(),
1068 version: "1.2.3".to_string(),
1069 license: "CC0-1.0".to_string(),
1070 provenance: CorePackProvenance {
1071 upstream_repo: "https://example.invalid/makehuman-data".to_string(),
1072 upstream_commit: "0123456789abcdef0123456789abcdef01234567".to_string(),
1073 files: vec![
1074 CorePackFile {
1075 name: "base.obj".to_string(),
1076 sha256: "a".repeat(64),
1077 },
1078 CorePackFile {
1079 name: "targets/smile.target".to_string(),
1080 sha256: "b".repeat(64),
1081 },
1082 ],
1083 },
1084 age_floor_years: Some(18.0),
1085 categories: vec!["body".to_string(), "face".to_string()],
1086 target_names: vec!["smile".to_string(), "blink".to_string()],
1087 }
1088 }
1089
1090 fn sample_base() -> (Vec<f32>, Vec<u32>, Vec<f32>) {
1092 let positions = vec![
1093 -1.0, -2.0, 0.5, 1.0, -2.0, 0.5, 1.0, 2.0, -0.5, -1.0, 2.0, -0.5, ];
1098 let indices = vec![0u32, 1, 2, 0, 2, 3];
1099 let uvs = vec![0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
1100 (positions, indices, uvs)
1101 }
1102
1103 fn build_sample() -> Vec<u8> {
1104 let (positions, indices, uvs) = sample_base();
1105 let mut b = CorePackBuilder::new();
1106 b.set_manifest(sample_manifest());
1107 b.set_base_mesh(&positions, &indices, Some(&uvs));
1108 b.add_target(
1110 "smile",
1111 "face",
1112 &[(1u32, [0.01, -0.02, 0.03]), (3u32, [-0.005, 0.004, 0.0])],
1113 );
1114 b.add_target("blink", "face", &[(2u32, [0.0, 0.0, 0.001])]);
1115 b.build().expect("build sample pack")
1116 }
1117
1118 #[test]
1119 fn manifest_exact_round_trip() {
1120 let bytes = build_sample();
1121 let pack = CorePack::parse(&bytes).expect("parse pack");
1122 assert_eq!(pack.manifest(), &sample_manifest());
1123 }
1124
1125 #[test]
1126 fn manifest_defaults_license_cc0() {
1127 let m = CorePackManifest::new("x", "1");
1128 assert_eq!(m.license, "CC0-1.0");
1129 }
1130
1131 #[test]
1132 fn base_positions_round_trip_within_bound() {
1133 let (positions, _indices, _uvs) = sample_base();
1134 let bytes = build_sample();
1135 let pack = CorePack::parse(&bytes).expect("parse pack");
1136
1137 let decoded = pack.base_positions();
1138 assert_eq!(decoded.len(), positions.len());
1139
1140 let mut actual_max = 0.0f32;
1143 for (o, d) in positions.iter().zip(&decoded) {
1144 actual_max = actual_max.max((o - d).abs());
1145 }
1146 let report = pack.quantization_report();
1147 assert!(
1149 report.base_pos_max_error_units + 1e-9 >= actual_max,
1150 "reported {} < actual {}",
1151 report.base_pos_max_error_units,
1152 actual_max
1153 );
1154 assert!(
1156 (report.base_pos_max_error_units - actual_max).abs() < 1e-6,
1157 "reported {} vs actual {}",
1158 report.base_pos_max_error_units,
1159 actual_max
1160 );
1161 assert!(
1163 (report.base_pos_max_error_mm - actual_max * MODEL_UNIT_MM).abs() < 1e-4,
1164 "mm conversion mismatch"
1165 );
1166
1167 let worst_half_step = 4.0f32 / I16_STEPS / 2.0;
1170 assert!(
1171 actual_max <= worst_half_step + 1e-6,
1172 "actual {} exceeds half-step {}",
1173 actual_max,
1174 worst_half_step
1175 );
1176 }
1177
1178 #[test]
1179 fn base_uvs_round_trip() {
1180 let (_p, _i, uvs) = sample_base();
1181 let bytes = build_sample();
1182 let pack = CorePack::parse(&bytes).expect("parse pack");
1183 let decoded = pack.base_uvs().expect("uvs present");
1184 assert_eq!(decoded.len(), uvs.len());
1185 for (o, d) in uvs.iter().zip(&decoded) {
1186 assert!((o - d).abs() < 1e-3, "uv {o} vs {d}");
1187 }
1188 }
1189
1190 #[test]
1191 fn base_indices_round_trip() {
1192 let (_p, indices, _u) = sample_base();
1193 let bytes = build_sample();
1194 let pack = CorePack::parse(&bytes).expect("parse pack");
1195 assert_eq!(pack.base_indices(), indices.as_slice());
1196 }
1197
1198 #[test]
1199 fn sparse_target_round_trip() {
1200 let bytes = build_sample();
1201 let pack = CorePack::parse(&bytes).expect("parse pack");
1202 assert_eq!(pack.targets().len(), 2);
1203
1204 let smile = &pack.targets()[0];
1205 assert_eq!(smile.name(), "smile");
1206 assert_eq!(smile.category(), "face");
1207 assert_eq!(smile.len(), 2);
1208
1209 let sparse = smile.sparse();
1210 assert_eq!(sparse[0].0, 1);
1212 assert_eq!(sparse[1].0, 3);
1213
1214 let report = pack.quantization_report();
1216 let smile_err = report
1217 .targets
1218 .iter()
1219 .find(|t| t.name == "smile")
1220 .map(|t| t.max_error_units)
1221 .expect("smile in report");
1222
1223 let expected = [
1224 (1u32, [0.01f32, -0.02, 0.03]),
1225 (3u32, [-0.005, 0.004, 0.0]),
1226 ];
1227 for ((idx, got), (eidx, want)) in sparse.iter().zip(expected.iter()) {
1228 assert_eq!(idx, eidx);
1229 for k in 0..3 {
1230 assert!(
1231 (got[k] - want[k]).abs() <= smile_err + 1e-6,
1232 "target comp {} got {} want {} err bound {}",
1233 k,
1234 got[k],
1235 want[k],
1236 smile_err
1237 );
1238 }
1239 }
1240 assert!(smile_err >= 0.0);
1242 let smile_mm = report
1243 .targets
1244 .iter()
1245 .find(|t| t.name == "smile")
1246 .map(|t| t.max_error_mm)
1247 .expect("smile mm");
1248 assert!((smile_mm - smile_err * MODEL_UNIT_MM).abs() < 1e-4);
1249 }
1250
1251 #[test]
1252 fn target_error_is_honest() {
1253 let bytes = build_sample();
1254 let pack = CorePack::parse(&bytes).expect("parse pack");
1255 let expected: std::collections::HashMap<u32, [f32; 3]> = [
1256 (1u32, [0.01f32, -0.02, 0.03]),
1257 (3u32, [-0.005, 0.004, 0.0]),
1258 ]
1259 .into_iter()
1260 .collect();
1261
1262 let smile = &pack.targets()[0];
1263 let mut actual_max = 0.0f32;
1264 for (idx, got) in smile.sparse() {
1265 let want = expected[&idx];
1266 for k in 0..3 {
1267 actual_max = actual_max.max((got[k] - want[k]).abs());
1268 }
1269 }
1270 assert!(smile.max_abs_error_units() + 1e-9 >= actual_max);
1272 assert!((smile.max_abs_error_units() - actual_max).abs() < 1e-6);
1273 }
1274
1275 #[test]
1276 fn deflate_round_trip_through_oxiarc() {
1277 let positions: Vec<f32> = (0..300).map(|i| (i as f32) * 0.01 - 1.5).collect();
1279 let indices: Vec<u32> = (0..99).collect();
1280 let mut sparse: Vec<(u32, [f32; 3])> = Vec::new();
1281 for i in (0..100).step_by(7) {
1282 sparse.push((i as u32, [0.002, -0.001, 0.0005]));
1283 }
1284 let mut b = CorePackBuilder::new();
1285 b.set_manifest(CorePackManifest::new("deflate-test", "1"));
1286 b.set_base_mesh(&positions, &indices, None);
1287 b.add_target("wave", "body", &sparse);
1288 let bytes = b.build().expect("build");
1289
1290 assert_eq!(&bytes[0..4], &OHPK_MAGIC);
1292 assert_eq!(bytes[4], OHPK_VERSION);
1293 assert_ne!(bytes[5] & OHPK_FLAG_DEFLATE, 0);
1294
1295 let pack = CorePack::parse(&bytes).expect("parse");
1296 assert_eq!(pack.vertex_count(), 100);
1297 assert_eq!(pack.base_indices().len(), 99);
1298 assert!(pack.base_uvs().is_none());
1299 assert_eq!(pack.targets().len(), 1);
1300 assert_eq!(pack.targets()[0].len(), sparse.len());
1301 }
1302
1303 #[test]
1304 fn empty_pack_build_errors() {
1305 let b = CorePackBuilder::new();
1306 assert!(b.build().is_err());
1307 }
1308
1309 #[test]
1310 fn odd_positions_length_errors() {
1311 let mut b = CorePackBuilder::new();
1312 b.set_base_mesh(&[0.0, 1.0], &[], None); assert!(b.build().is_err());
1314 }
1315
1316 #[test]
1317 fn mismatched_uv_length_errors() {
1318 let mut b = CorePackBuilder::new();
1319 b.set_base_mesh(&[0.0, 0.0, 0.0], &[0], Some(&[0.0])); assert!(b.build().is_err());
1321 }
1322
1323 #[test]
1324 fn bad_magic_rejected() {
1325 let mut bytes = build_sample();
1326 bytes[0] = b'X';
1327 assert!(CorePack::parse(&bytes).is_err());
1328 }
1329
1330 #[test]
1331 fn bad_version_rejected() {
1332 let mut bytes = build_sample();
1333 bytes[4] = 99;
1334 assert!(CorePack::parse(&bytes).is_err());
1335 }
1336
1337 #[test]
1338 fn truncated_header_rejected() {
1339 let bytes = build_sample();
1340 assert!(CorePack::parse(&bytes[0..6]).is_err());
1341 }
1342
1343 #[test]
1344 fn truncated_body_rejected() {
1345 let bytes = build_sample();
1346 let cut = 10 + (bytes.len() - 10) / 2;
1348 assert!(CorePack::parse(&bytes[0..cut]).is_err());
1349 }
1350
1351 #[test]
1352 fn oversized_declared_length_rejected() {
1353 let mut bytes = build_sample();
1354 bytes[6] = 0xff;
1356 bytes[7] = 0xff;
1357 bytes[8] = 0xff;
1358 bytes[9] = 0x7f;
1359 let res = CorePack::parse(&bytes);
1360 assert!(res.is_err());
1361 }
1362
1363 #[test]
1364 fn empty_input_rejected() {
1365 assert!(CorePack::parse(&[]).is_err());
1366 }
1367
1368 #[test]
1369 fn helper_metadata_round_trip() {
1370 let (positions, indices, _uvs) = sample_base();
1371 let mut b = CorePackBuilder::new();
1372 b.set_manifest(CorePackManifest::new("helper", "1"));
1373 b.set_base_mesh(&positions, &indices, None);
1374 b.set_helper_metadata(&[1, 2, 3, 4, 5]);
1375 let bytes = b.build().expect("build");
1376 let pack = CorePack::parse(&bytes).expect("parse");
1377 assert_eq!(pack.helper_metadata(), Some(&[1u8, 2, 3, 4, 5][..]));
1378 }
1379
1380 #[test]
1381 fn target_names_auto_filled_when_empty() {
1382 let (positions, indices, _uvs) = sample_base();
1383 let mut manifest = CorePackManifest::new("auto", "1");
1384 manifest.target_names.clear();
1385 let mut b = CorePackBuilder::new();
1386 b.set_manifest(manifest);
1387 b.set_base_mesh(&positions, &indices, None);
1388 b.add_target("a", "cat", &[(0u32, [0.1, 0.0, 0.0])]);
1389 b.add_target("b", "cat", &[(1u32, [0.0, 0.1, 0.0])]);
1390 let bytes = b.build().expect("build");
1391 let pack = CorePack::parse(&bytes).expect("parse");
1392 assert_eq!(pack.manifest().target_names, vec!["a", "b"]);
1393 }
1394
1395 #[test]
1396 fn size_target_reasonable() {
1397 let n = 2000usize;
1399 let positions: Vec<f32> = (0..n * 3).map(|i| ((i % 97) as f32) * 0.01).collect();
1400 let indices: Vec<u32> = (0..(n as u32 - 1) * 3).map(|i| i % n as u32).collect();
1401 let mut b = CorePackBuilder::new();
1402 b.set_manifest(CorePackManifest::new("size", "1"));
1403 b.set_base_mesh(&positions, &indices, None);
1404 for t in 0..40 {
1405 let sparse: Vec<(u32, [f32; 3])> = (0..50)
1406 .map(|k| ((t * 50 + k) as u32 % n as u32, [0.001, 0.001, 0.001]))
1407 .collect();
1408 b.add_target(format!("t{t}"), "body", &sparse);
1409 }
1410 let bytes = b.build().expect("build");
1411 assert!(bytes.len() < 2 * 1024 * 1024, "pack size {}", bytes.len());
1412 }
1413}