Skip to main content

draco_gltf/
writer.rs

1//! Atomic writing of materialized primitive geometry into lossless documents.
2
3use crate::json::Value;
4use crate::{
5    ComponentType, Error, GeometryError, Import, MeshIndex, PackedAttribute, PackedGeometry,
6    PrimitiveIndex, Result, ValidationProfile,
7};
8
9/// Storage selected for newly written primitive geometry.
10#[derive(Clone, Copy, Debug, Default)]
11pub enum GeometryEncoding {
12    /// Store geometry in ordinary tightly packed glTF accessors.
13    #[default]
14    Raw,
15    /// Encode geometry with `KHR_draco_mesh_compression`.
16    #[cfg(feature = "draco-encode")]
17    Draco(crate::CompressionOptions),
18}
19
20/// Options controlling one geometry write operation.
21#[derive(Clone, Copy, Debug, Default)]
22pub struct GeometryWriteOptions {
23    /// Binary storage used by the destination primitive.
24    pub encoding: GeometryEncoding,
25}
26
27/// Why source resource bytes were conservatively retained after a write.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum PreserveReason {
30    /// Existing resources may still be referenced by another core object or
31    /// by an extension whose binary-reference semantics are not remapped.
32    ExistingReferences,
33}
34
35/// Measured result of one atomic geometry write.
36#[derive(Clone, Debug)]
37pub struct GeometryWriteReport {
38    /// Primitive changed by the operation.
39    pub primitive: PrimitiveIndex,
40    /// Storage selected by the caller.
41    pub encoding: GeometryEncoding,
42    /// Total resolved bytes before the operation.
43    pub source_bytes: usize,
44    /// Total resolved bytes after the operation.
45    pub output_bytes: usize,
46    /// Bytes in the newly generated geometry payload.
47    pub encoded_bytes: usize,
48    /// Bytes proven unreachable and removed.
49    pub reclaimed_bytes: usize,
50    /// Reasons preventing more aggressive source-resource reclamation.
51    pub preserve_reasons: Vec<PreserveReason>,
52}
53
54impl Import {
55    /// Replaces one primitive's geometry atomically.
56    ///
57    /// Raw storage is the default. The existing material, `extras`, morph
58    /// targets with the same vertex count, and unrelated extensions remain on
59    /// the primitive. Shared source accessors are never modified in place.
60    pub fn write_primitive(
61        &mut self,
62        primitive: PrimitiveIndex,
63        geometry: &PackedGeometry,
64        options: GeometryWriteOptions,
65    ) -> Result<GeometryWriteReport> {
66        geometry.validate(self.validation_profile())?;
67        let mut candidate = self.clone();
68        let source_bytes = total_bytes(&candidate)?;
69        let raw_bytes = candidate.write_raw_primitive_inner(primitive, geometry)?;
70        #[allow(unused_mut)]
71        let mut report = GeometryWriteReport {
72            primitive,
73            encoding: options.encoding,
74            source_bytes,
75            output_bytes: total_bytes(&candidate)?,
76            encoded_bytes: raw_bytes,
77            reclaimed_bytes: 0,
78            preserve_reasons: if source_bytes == 0 {
79                Vec::new()
80            } else {
81                vec![PreserveReason::ExistingReferences]
82            },
83        };
84        #[cfg(feature = "draco-encode")]
85        if let GeometryEncoding::Draco(draco) = options.encoding {
86            let compressed =
87                candidate.compress_primitive(primitive.mesh, primitive.primitive, draco)?;
88            report.output_bytes = compressed.output_bytes;
89            report.encoded_bytes = compressed.encoded_bytes;
90            report.reclaimed_bytes = compressed.reclaimed_bytes;
91            if compressed.reclaimed_bytes > 0 {
92                report.preserve_reasons.clear();
93            }
94        }
95        candidate.validate_after_write()?;
96        *self = candidate;
97        Ok(report)
98    }
99
100    /// Appends packed geometry to an existing mesh atomically.
101    ///
102    /// Returns the stable location accepted by [`Import::read_primitive`] and
103    /// [`Import::write_primitive`].
104    pub fn push_primitive(
105        &mut self,
106        mesh: MeshIndex,
107        geometry: &PackedGeometry,
108        options: GeometryWriteOptions,
109    ) -> Result<PrimitiveIndex> {
110        geometry.validate(self.validation_profile())?;
111        let mut candidate = self.clone();
112        let primitives = candidate
113            .document
114            .as_value_mut()
115            .get_mut("meshes")
116            .and_then(Value::as_array_mut)
117            .and_then(|meshes| meshes.get_mut(mesh.0))
118            .and_then(|mesh| mesh.get_mut("primitives"))
119            .and_then(Value::as_array_mut)
120            .ok_or_else(|| Error::Validation(vec!["mesh primitives are invalid".into()]))?;
121        let primitive = PrimitiveIndex::new(mesh, primitives.len());
122        primitives.push(Value::object([("attributes", Value::Object(Vec::new()))]));
123        candidate.write_primitive(primitive, geometry, options)?;
124        *self = candidate;
125        Ok(primitive)
126    }
127
128    /// Creates a minimal scene containing one packed primitive.
129    ///
130    /// ```
131    /// use draco_gltf::{
132    ///     ComponentType, GeometryWriteOptions, Import, PackedAttribute,
133    ///     PackedGeometry, PrimitiveMode, ValidationProfile,
134    /// };
135    ///
136    /// let position = PackedAttribute::new(
137    ///     "POSITION", 1, 3, ComponentType::F32, false, vec![0; 12],
138    /// )?;
139    /// let geometry = PackedGeometry::new(PrimitiveMode::Points, vec![position], None)?;
140    /// let scene = Import::from_geometry(
141    ///     &geometry,
142    ///     ValidationProfile::Gltf20,
143    ///     GeometryWriteOptions::default(),
144    /// )?;
145    /// assert_eq!(scene.document.meshes().len(), 1);
146    /// # Ok::<(), draco_gltf::Error>(())
147    /// ```
148    pub fn from_geometry(
149        geometry: &PackedGeometry,
150        profile: ValidationProfile,
151        options: GeometryWriteOptions,
152    ) -> Result<Self> {
153        geometry.validate(profile)?;
154        let version = match profile {
155            ValidationProfile::Gltf20 => "2.0",
156            ValidationProfile::Gltf21Draft => "2.1",
157        };
158        let document = format!(
159            "{{\"asset\":{{\"version\":\"{version}\"}},\"buffers\":[],\"bufferViews\":[],\"accessors\":[],\"meshes\":[{{\"primitives\":[]}}],\"nodes\":[{{\"mesh\":0}}],\"scenes\":[{{\"nodes\":[0]}}],\"scene\":0}}"
160        );
161        let mut import = crate::parse(document.as_bytes(), profile)?;
162        import.push_primitive(MeshIndex(0), geometry, options)?;
163        Ok(import)
164    }
165
166    pub(crate) fn write_raw_primitive_inner(
167        &mut self,
168        location: PrimitiveIndex,
169        geometry: &PackedGeometry,
170    ) -> Result<usize> {
171        let primitive = self
172            .document
173            .primitive(location.mesh, location.primitive)
174            .ok_or_else(|| Error::Validation(vec!["primitive is out of range".into()]))?;
175        validate_morph_targets(self, primitive, geometry.vertex_count())?;
176
177        let buffer_index = self.resources.buffers.len();
178        let mut bytes = Vec::new();
179        let mut views = Vec::new();
180        for attribute in geometry.attributes() {
181            pad_to_four(&mut bytes);
182            let offset = bytes.len();
183            bytes.extend_from_slice(attribute.bytes());
184            views.push((offset, attribute.bytes().len(), 34962u32));
185        }
186        let index_view = if let Some(indices) = geometry.indices() {
187            pad_to_four(&mut bytes);
188            let offset = bytes.len();
189            bytes.extend_from_slice(indices.bytes());
190            views.push((offset, indices.bytes().len(), 34963u32));
191            Some(views.len() - 1)
192        } else {
193            None
194        };
195        let encoded_bytes = bytes.len();
196
197        let root = self.document.as_value_mut();
198        ensure_root_array(root, "buffers")?
199            .push(Value::object([("byteLength", Value::from(bytes.len()))]));
200        let first_view = ensure_root_array(root, "bufferViews")?.len();
201        for (offset, length, target) in &views {
202            ensure_root_array(root, "bufferViews")?.push(Value::object([
203                ("buffer", Value::from(buffer_index)),
204                ("byteOffset", Value::from(*offset)),
205                ("byteLength", Value::from(*length)),
206                ("target", Value::from(*target as u64)),
207            ]));
208        }
209
210        let first_accessor = ensure_root_array(root, "accessors")?.len();
211        for (offset, attribute) in geometry.attributes().iter().enumerate() {
212            let mut accessor = Value::object([
213                ("bufferView", Value::from(first_view + offset)),
214                (
215                    "componentType",
216                    Value::from(attribute.component_type().to_gltf() as u64),
217                ),
218                ("count", Value::from(attribute.count())),
219                ("type", Value::from(accessor_type(attribute.components()))),
220            ]);
221            if attribute.normalized() {
222                accessor["normalized"] = Value::Bool(true);
223            }
224            if attribute.semantic() == "POSITION" {
225                let (min, max) = position_bounds(attribute)?;
226                accessor["min"] = Value::Array(min);
227                accessor["max"] = Value::Array(max);
228            }
229            ensure_root_array(root, "accessors")?.push(accessor);
230        }
231        let index_accessor = if let (Some(indices), Some(view)) = (geometry.indices(), index_view) {
232            let index = ensure_root_array(root, "accessors")?.len();
233            ensure_root_array(root, "accessors")?.push(Value::object([
234                ("bufferView", Value::from(first_view + view)),
235                (
236                    "componentType",
237                    Value::from(indices.component_type().to_gltf() as u64),
238                ),
239                ("count", Value::from(indices.count())),
240                ("type", Value::from("SCALAR")),
241            ]));
242            Some(index)
243        } else {
244            None
245        };
246
247        let primitive = root["meshes"][location.mesh.0]["primitives"]
248            .as_array_mut()
249            .and_then(|primitives| primitives.get_mut(location.primitive))
250            .ok_or_else(|| Error::Validation(vec!["primitive changed during write".into()]))?;
251        primitive["mode"] = Value::from(geometry.mode().to_gltf() as u64);
252        primitive["attributes"] = Value::Object(
253            geometry
254                .attributes()
255                .iter()
256                .enumerate()
257                .map(|(offset, attribute)| {
258                    (
259                        attribute.semantic().to_owned(),
260                        Value::from(first_accessor + offset),
261                    )
262                })
263                .collect(),
264        );
265        if let Some(index) = index_accessor {
266            primitive["indices"] = Value::from(index);
267        } else {
268            remove_key(primitive, "indices");
269        }
270        remove_draco_extension(primitive);
271        remove_unused_draco_name(root);
272        self.resources.buffers.push(bytes);
273        Ok(encoded_bytes)
274    }
275}
276
277#[derive(Clone, Copy)]
278enum BoundScalar {
279    Signed(i64),
280    Unsigned(u64),
281    Float(f64),
282}
283
284impl BoundScalar {
285    fn is_less_than(self, other: Self) -> bool {
286        match (self, other) {
287            (Self::Signed(left), Self::Signed(right)) => left < right,
288            (Self::Unsigned(left), Self::Unsigned(right)) => left < right,
289            (Self::Float(left), Self::Float(right)) => left < right,
290            _ => unreachable!("one accessor cannot mix component types"),
291        }
292    }
293
294    fn into_json(self) -> Value {
295        let lexeme = match self {
296            Self::Signed(value) => value.to_string(),
297            Self::Unsigned(value) => value.to_string(),
298            Self::Float(value) => finite_float_lexeme(value),
299        };
300        Value::Number(lexeme)
301    }
302}
303
304pub(crate) fn finite_float_lexeme(value: f64) -> String {
305    let bits = value.to_bits();
306    let negative = bits >> 63 != 0;
307    let exponent = ((bits >> 52) & 0x7ff) as i32;
308    let fraction = bits & ((1u64 << 52) - 1);
309    if exponent == 0 && fraction == 0 {
310        return if negative { "-0" } else { "0" }.into();
311    }
312    let (significand, binary_exponent) = if exponent == 0 {
313        (fraction, -1074)
314    } else {
315        ((1u64 << 52) | fraction, exponent - 1023 - 52)
316    };
317    let mut digits = significand
318        .to_string()
319        .bytes()
320        .rev()
321        .map(|digit| digit - b'0')
322        .collect::<Vec<_>>();
323    let mut scale = 0usize;
324    if binary_exponent >= 0 {
325        for _ in 0..binary_exponent {
326            multiply_decimal(&mut digits, 2);
327        }
328    } else {
329        scale = (-binary_exponent) as usize;
330        for _ in 0..scale {
331            multiply_decimal(&mut digits, 5);
332        }
333        while scale > 0 && digits.first() == Some(&0) {
334            digits.remove(0);
335            scale -= 1;
336        }
337    }
338    let mut out = String::with_capacity(digits.len() + 3 + scale.saturating_sub(digits.len()));
339    if negative {
340        out.push('-');
341    }
342    if scale == 0 {
343        out.extend(digits.iter().rev().map(|digit| char::from(b'0' + digit)));
344    } else if digits.len() > scale {
345        for (index, digit) in digits.iter().rev().enumerate() {
346            if index == digits.len() - scale {
347                out.push('.');
348            }
349            out.push(char::from(b'0' + digit));
350        }
351    } else {
352        out.push_str("0.");
353        out.extend(std::iter::repeat_n('0', scale - digits.len()));
354        out.extend(digits.iter().rev().map(|digit| char::from(b'0' + digit)));
355    }
356    out
357}
358
359fn multiply_decimal(digits: &mut Vec<u8>, factor: u8) {
360    let mut carry = 0u16;
361    for digit in digits.iter_mut() {
362        let value = u16::from(*digit) * u16::from(factor) + carry;
363        *digit = (value % 10) as u8;
364        carry = value / 10;
365    }
366    while carry != 0 {
367        digits.push((carry % 10) as u8);
368        carry /= 10;
369    }
370}
371
372fn position_bounds(attribute: &PackedAttribute) -> Result<(Vec<Value>, Vec<Value>)> {
373    let scalar_width = attribute.component_type().byte_width();
374    let row_width = scalar_width
375        .checked_mul(attribute.components() as usize)
376        .ok_or(GeometryError::ByteSizeOverflow)?;
377    let first = attribute
378        .bytes()
379        .get(..row_width)
380        .ok_or(GeometryError::EmptyGeometry)?;
381    let mut min = (0..attribute.components())
382        .map(|component| {
383            read_bound_scalar(
384                first,
385                component as usize * scalar_width,
386                attribute.component_type(),
387            )
388        })
389        .collect::<std::result::Result<Vec<_>, _>>()?;
390    let mut max = min.clone();
391    for row in attribute.bytes().chunks_exact(row_width).skip(1) {
392        for component in 0..attribute.components() as usize {
393            let value =
394                read_bound_scalar(row, component * scalar_width, attribute.component_type())?;
395            if value.is_less_than(min[component]) {
396                min[component] = value;
397            }
398            if max[component].is_less_than(value) {
399                max[component] = value;
400            }
401        }
402    }
403    Ok((
404        min.into_iter().map(BoundScalar::into_json).collect(),
405        max.into_iter().map(BoundScalar::into_json).collect(),
406    ))
407}
408
409fn read_bound_scalar(
410    bytes: &[u8],
411    offset: usize,
412    component_type: ComponentType,
413) -> std::result::Result<BoundScalar, GeometryError> {
414    let bytes = &bytes[offset..offset + component_type.byte_width()];
415    let scalar = match component_type {
416        ComponentType::I8 => BoundScalar::Signed(bytes[0] as i8 as i64),
417        ComponentType::U8 => BoundScalar::Unsigned(bytes[0] as u64),
418        ComponentType::I16 => {
419            BoundScalar::Signed(i16::from_le_bytes(bytes.try_into().unwrap()) as i64)
420        }
421        ComponentType::U16 => {
422            BoundScalar::Unsigned(u16::from_le_bytes(bytes.try_into().unwrap()) as u64)
423        }
424        ComponentType::I32 => {
425            BoundScalar::Signed(i32::from_le_bytes(bytes.try_into().unwrap()) as i64)
426        }
427        ComponentType::U32 => {
428            BoundScalar::Unsigned(u32::from_le_bytes(bytes.try_into().unwrap()) as u64)
429        }
430        ComponentType::F32 => {
431            BoundScalar::Float(f32::from_le_bytes(bytes.try_into().unwrap()) as f64)
432        }
433        ComponentType::F16 => {
434            BoundScalar::Float(half_to_f32(u16::from_le_bytes(bytes.try_into().unwrap())) as f64)
435        }
436        ComponentType::F64 => BoundScalar::Float(f64::from_le_bytes(bytes.try_into().unwrap())),
437        ComponentType::I64 => BoundScalar::Signed(i64::from_le_bytes(bytes.try_into().unwrap())),
438        ComponentType::U64 => BoundScalar::Unsigned(u64::from_le_bytes(bytes.try_into().unwrap())),
439    };
440    if matches!(scalar, BoundScalar::Float(value) if !value.is_finite()) {
441        return Err(GeometryError::NonFinitePosition);
442    }
443    Ok(scalar)
444}
445
446fn half_to_f32(bits: u16) -> f32 {
447    let sign = ((bits & 0x8000) as u32) << 16;
448    let exponent = (bits >> 10) & 0x1f;
449    let fraction = (bits & 0x03ff) as u32;
450    let value = match exponent {
451        0 if fraction == 0 => sign,
452        0 => {
453            let leading = fraction.leading_zeros() - 22;
454            let normalized = fraction << (leading + 1);
455            let exponent = 127 - 15 - leading;
456            sign | (exponent << 23) | ((normalized & 0x03ff) << 13)
457        }
458        0x1f => sign | 0x7f80_0000 | (fraction << 13),
459        _ => sign | ((exponent as u32 + 112) << 23) | (fraction << 13),
460    };
461    f32::from_bits(value)
462}
463
464fn total_bytes(import: &Import) -> Result<usize> {
465    import
466        .resources
467        .buffers
468        .iter()
469        .try_fold(0usize, |total, bytes| {
470            total
471                .checked_add(bytes.len())
472                .ok_or_else(|| Error::ResourceLimit("total resource size overflow".into()))
473        })
474}
475
476fn validate_morph_targets(
477    import: &Import,
478    primitive: crate::PrimitiveRef<'_>,
479    vertex_count: usize,
480) -> Result<()> {
481    for target in primitive.morph_targets() {
482        for (_, accessor) in target {
483            let count = accessor
484                .as_u64()
485                .and_then(|index| usize::try_from(index).ok())
486                .and_then(|index| import.document.accessor(crate::AccessorIndex(index)))
487                .and_then(|accessor| accessor.count())
488                .and_then(|count| usize::try_from(count).ok())
489                .ok_or_else(|| {
490                    Error::Validation(vec!["morph target accessor is invalid".into()])
491                })?;
492            if count != vertex_count {
493                return Err(Error::Geometry(crate::GeometryError::MorphTargetCount {
494                    expected: count,
495                    actual: vertex_count,
496                }));
497            }
498        }
499    }
500    Ok(())
501}
502
503fn ensure_root_array<'a>(root: &'a mut Value, name: &str) -> Result<&'a mut Vec<Value>> {
504    if root.get(name).is_none() {
505        root[name] = Value::Array(Vec::new());
506    }
507    root.get_mut(name)
508        .and_then(Value::as_array_mut)
509        .ok_or_else(|| Error::Validation(vec![format!("{name} is not an array")]))
510}
511
512fn accessor_type(components: u8) -> &'static str {
513    match components {
514        1 => "SCALAR",
515        2 => "VEC2",
516        3 => "VEC3",
517        4 => "VEC4",
518        _ => unreachable!("PackedAttribute validates component counts"),
519    }
520}
521
522fn pad_to_four(bytes: &mut Vec<u8>) {
523    while !bytes.len().is_multiple_of(4) {
524        bytes.push(0);
525    }
526}
527
528fn remove_key(value: &mut Value, key: &str) {
529    if let Some(entries) = value.as_object_mut() {
530        entries.retain(|(name, _)| name != key);
531    }
532}
533
534fn remove_draco_extension(primitive: &mut Value) {
535    let Some(extensions) = primitive
536        .get_mut("extensions")
537        .and_then(Value::as_object_mut)
538    else {
539        return;
540    };
541    extensions.retain(|(name, _)| name != crate::KHR_DRACO_MESH_COMPRESSION);
542    if extensions.is_empty() {
543        remove_key(primitive, "extensions");
544    }
545}
546
547fn remove_unused_draco_name(root: &mut Value) {
548    let still_used = root
549        .get("meshes")
550        .and_then(Value::as_array)
551        .into_iter()
552        .flatten()
553        .filter_map(|mesh| mesh.get("primitives").and_then(Value::as_array))
554        .flatten()
555        .any(|primitive| {
556            primitive
557                .get("extensions")
558                .and_then(|extensions| extensions.get(crate::KHR_DRACO_MESH_COMPRESSION))
559                .is_some()
560        });
561    if still_used {
562        return;
563    }
564    for name in ["extensionsUsed", "extensionsRequired"] {
565        if let Some(values) = root.get_mut(name).and_then(Value::as_array_mut) {
566            values.retain(|value| value.as_str() != Some(crate::KHR_DRACO_MESH_COMPRESSION));
567            if values.is_empty() {
568                remove_key(root, name);
569            }
570        }
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::finite_float_lexeme;
577
578    #[test]
579    fn exact_float_lexemes_roundtrip() {
580        for value in [
581            0.0,
582            -0.0,
583            0.1,
584            -12345.75,
585            f64::MIN_POSITIVE,
586            f64::from_bits(1),
587            f64::MAX,
588        ] {
589            let parsed = finite_float_lexeme(value).parse::<f64>().unwrap();
590            assert_eq!(parsed.to_bits(), value.to_bits());
591        }
592    }
593}