Skip to main content

draco_oxide/encode/
mod.rs

1pub(crate) mod attribute;
2pub(crate) mod config_spec;
3pub(crate) mod connectivity;
4pub(crate) mod ds;
5pub(crate) mod entropy;
6pub(crate) mod header;
7pub(crate) mod metadata;
8/// Point-cloud encoding: the kd-tree method and its configuration.
9pub mod point_cloud;
10
11use draco_oxide_core::bit_coder::ByteWriter;
12use draco_oxide_core::debug_write;
13use draco_oxide_core::mesh::Mesh;
14use draco_oxide_core::point_cloud::PointCloud;
15use draco_oxide_core::types::ConfigType;
16use thiserror::Error;
17
18/// Per-attribute encoding configuration and its option types.
19pub use attribute::{AttributeConfig, NormalEncoding, Quantization};
20/// Configuration for edgebreaker connectivity encoding.
21pub use connectivity::edgebreaker::Config as EdgebreakerConfig;
22/// Configuration for sequential connectivity encoding.
23pub use connectivity::sequential::Config as SequentialConfig;
24/// Selection of the connectivity encoding method and its configuration.
25pub use connectivity::Config as ConnectivityConfig;
26/// Configuration for point-cloud encoding.
27pub use point_cloud::Config as PointCloudConfig;
28
29use config_spec::ConfigSpec;
30
31/// The encoder configuration: connectivity method, per-attribute encoding
32/// options, geometry type, and metadata toggle. Built with the `with_*`
33/// builder methods, or deserialized from TOML.
34#[derive(Debug, Clone, serde::Deserialize)]
35#[serde(from = "ConfigSpec")]
36pub struct Config {
37    // Connectivity compression method and its config (edgebreaker or sequential).
38    // Also the single source of truth for the header's encoder method.
39    connectivity: connectivity::Config,
40    // Per-attribute encoding configuration (see `attribute::Config`).
41    attribute: attribute::Config,
42    geometry_type: header::EncodedGeometryType,
43    metadata: bool,
44}
45
46impl ConfigType for Config {
47    fn default() -> Self {
48        Self {
49            connectivity: connectivity::Config::default(),
50            attribute: attribute::Config::default(),
51            geometry_type: header::EncodedGeometryType::TrianglarMesh,
52            metadata: false,
53        }
54    }
55}
56
57impl Config {
58    /// Sets how normal attributes are compressed.
59    ///
60    /// [`NormalEncoding::PredictedOnly`] makes normal compression effectively
61    /// zero-CPU: the encoder ignores the input normal values (using only their
62    /// seam topology) and emits an all-zero correction stream, so the decoder
63    /// reconstructs exactly the normals it predicts from the geometry.
64    ///
65    /// ```no_run
66    /// # use draco_oxide::core::types::ConfigType;
67    /// use draco_oxide::encode::{Config, NormalEncoding};
68    /// let cfg = Config::default().with_normals(NormalEncoding::PredictedOnly);
69    /// ```
70    pub fn with_normals(mut self, enc: NormalEncoding) -> Self {
71        self.attribute.set_normal_encoding(enc);
72        self
73    }
74
75    /// Overrides the per-type encoding for `ty` (prediction scheme, transform,
76    /// quantization, and the normal encoding mode for normals). Absent knobs
77    /// fall back to the built-in default for that attribute type.
78    pub fn with_attribute(
79        mut self,
80        ty: draco_oxide_core::attribute::AttributeType,
81        cfg: AttributeConfig,
82    ) -> Self {
83        self.attribute.set(ty, cfg);
84        self
85    }
86
87    /// Selects the connectivity compression method and its configuration.
88    pub fn with_connectivity(mut self, cfg: ConnectivityConfig) -> Self {
89        self.connectivity = cfg;
90        self
91    }
92
93    /// Selects edgebreaker connectivity compression with the given config.
94    pub fn with_edgebreaker(mut self, cfg: EdgebreakerConfig) -> Self {
95        self.connectivity = ConnectivityConfig::Edgebreaker(cfg);
96        self
97    }
98
99    /// Selects sequential connectivity compression with the given config.
100    pub fn with_sequential(mut self, cfg: SequentialConfig) -> Self {
101        self.connectivity = ConnectivityConfig::Sequential(cfg);
102        self
103    }
104
105    /// Enables or disables metadata encoding.
106    pub fn with_metadata(mut self, metadata: bool) -> Self {
107        self.metadata = metadata;
108        self
109    }
110
111    /// The current per-type attribute override for `ty` (empty default if none),
112    /// for read-modify-write layering of overrides (e.g. a CLI flag patching a
113    /// single knob on top of a file-loaded config).
114    pub fn attribute_config(
115        &self,
116        ty: draco_oxide_core::attribute::AttributeType,
117    ) -> AttributeConfig {
118        self.attribute.get(ty)
119    }
120
121    /// The selected connectivity configuration.
122    pub fn connectivity(&self) -> &ConnectivityConfig {
123        &self.connectivity
124    }
125
126    /// Validates the configuration for internal consistency, rejecting
127    /// combinations that would produce an undecodable or nonsensical stream (a
128    /// texture predictor on a normal attribute, a coordinate max-error on an
129    /// octahedral normal, an unimplemented traversal, out-of-range bits, …).
130    /// Called automatically by every encode entry point.
131    pub fn validate(&self) -> Result<(), ConfigError> {
132        use draco_oxide_core::attribute::AttributeType;
133        use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType;
134        use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
135
136        // Connectivity: reject the unimplemented predictive edgebreaker traversal.
137        if let ConnectivityConfig::Edgebreaker(eb) = &self.connectivity {
138            if eb.traversal == EdgebreakerKind::Predictive {
139                return Err(ConfigError::UnsupportedTraversal);
140            }
141        }
142        let sequential = matches!(self.connectivity, ConnectivityConfig::Sequential(_));
143
144        for (&ty, over) in self.attribute.overrides() {
145            // A sequential stream carries no connectivity, so nothing can
146            // predict from the mesh: the built-in mesh defaults degrade to
147            // delta, but an explicit request for one cannot be honored, and
148            // trusting a geometry-derived normal prediction is meaningless.
149            if sequential {
150                if let Some(scheme) = &over.prediction {
151                    if !matches!(
152                        scheme,
153                        PredictionSchemeType::DeltaPrediction | PredictionSchemeType::NoPrediction
154                    ) {
155                        return Err(ConfigError::MeshPredictionUnderSequential(format!(
156                            "{scheme:?}"
157                        )));
158                    }
159                }
160                if over.normal_encoding == Some(NormalEncoding::PredictedOnly) {
161                    return Err(ConfigError::PredictedNormalsUnderSequential);
162                }
163                if over.traversal
164                    == Some(draco_oxide_core::codec::connectivity::edgebreaker::TraversalType::PredictionDegree)
165                {
166                    return Err(ConfigError::PredictionDegreeUnderSequential);
167                }
168            }
169
170            if over.normal_encoding.is_some() && ty != AttributeType::Normal {
171                return Err(ConfigError::NormalEncodingOnNonNormal(ty));
172            }
173
174            if let Some(scheme) = &over.prediction {
175                if !allowed_schemes(ty).iter().any(|s| s == scheme) {
176                    return Err(ConfigError::PredictionSchemeForType {
177                        ty,
178                        scheme: format!("{scheme:?}"),
179                    });
180                }
181            }
182
183            if let Some(transform) = over.transform {
184                if !allowed_transforms(ty).contains(&transform) {
185                    return Err(ConfigError::TransformForType {
186                        ty,
187                        transform: format!("{transform:?}"),
188                    });
189                }
190                // The wire frames NoPrediction without any transform, so a
191                // transform override cannot be honored alongside it.
192                if over.prediction == Some(PredictionSchemeType::NoPrediction)
193                    && transform != attribute::PredictionTransformType::NoTransform
194                {
195                    return Err(ConfigError::TransformWithNoPrediction);
196                }
197            }
198
199            if let Some(quant) = over.quantization {
200                // Octahedral (normal) resolution is angular, not coordinate; only
201                // an explicit bit count is meaningful there.
202                if ty == AttributeType::Normal && !matches!(quant, Quantization::Bits(_)) {
203                    return Err(ConfigError::NonBitsQuantizationForNormal);
204                }
205                if let Quantization::Bits(n) = quant {
206                    if !(1..=30).contains(&n) {
207                        return Err(ConfigError::QuantizationBitsOutOfRange(n));
208                    }
209                }
210            }
211        }
212
213        Ok(())
214    }
215}
216
217/// The prediction schemes valid for a given attribute type.
218fn allowed_schemes(
219    ty: draco_oxide_core::attribute::AttributeType,
220) -> Vec<draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType> {
221    use draco_oxide_core::attribute::AttributeType::*;
222    use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType as S;
223    match ty {
224        Position => vec![
225            S::MeshParallelogramPrediction,
226            S::MeshConstrainedMultiParallelogramPrediction,
227            S::DeltaPrediction,
228            S::NoPrediction,
229        ],
230        Normal => vec![S::MeshNormalPrediction],
231        TextureCoordinate => vec![
232            S::MeshParallelogramPrediction,
233            S::MeshConstrainedMultiParallelogramPrediction,
234            S::MeshPredictionForTextureCoordinates,
235            S::DeltaPrediction,
236            S::NoPrediction,
237        ],
238        // Color, Custom, and any other generic per-vertex attribute have no
239        // geometry-derived predictor, but the parallelogram family predicts
240        // any value carried over the mesh connectivity.
241        _ => vec![
242            S::MeshConstrainedMultiParallelogramPrediction,
243            S::DeltaPrediction,
244            S::NoPrediction,
245        ],
246    }
247}
248
249/// The prediction transforms valid for a given attribute type.
250fn allowed_transforms(
251    ty: draco_oxide_core::attribute::AttributeType,
252) -> Vec<attribute::PredictionTransformType> {
253    use attribute::PredictionTransformType as T;
254    use draco_oxide_core::attribute::AttributeType::*;
255    match ty {
256        // Normals ride the octahedral transform.
257        Normal => vec![T::OctahedralOrthogonal],
258        _ => vec![T::Difference, T::WrappedDifference, T::NoTransform],
259    }
260}
261
262/// Errors from [`Config::validate`].
263#[remain::sorted]
264#[derive(Error, Debug)]
265pub enum ConfigError {
266    /// A mesh-based prediction scheme was requested under sequential
267    /// connectivity encoding, which carries no connectivity to predict from.
268    #[error("prediction scheme {0} needs mesh connectivity, which sequential encoding omits")]
269    MeshPredictionUnderSequential(String),
270    /// A quantization mode other than an explicit bit count was set on a
271    /// normal attribute.
272    #[error("normals accept only an explicit bit count (octahedral error is angular)")]
273    NonBitsQuantizationForNormal,
274    /// A normal encoding mode was set on an attribute that is not a normal.
275    #[error("normal encoding was set on a non-normal attribute ({0:?})")]
276    NormalEncodingOnNonNormal(draco_oxide_core::attribute::AttributeType),
277    /// Geometry-predicted normals were requested under sequential connectivity
278    /// encoding, which carries no connectivity to predict from.
279    #[error("geometry-predicted normals need mesh connectivity, which sequential encoding omits")]
280    PredictedNormalsUnderSequential,
281    /// Prediction-degree traversal was requested under sequential connectivity
282    /// encoding, which carries no connectivity to traverse.
283    #[error(
284        "prediction-degree traversal needs mesh connectivity, which sequential encoding omits"
285    )]
286    PredictionDegreeUnderSequential,
287    /// The requested prediction scheme is not valid for the attribute type.
288    #[error("prediction scheme {scheme} is not valid for attribute type {ty:?}")]
289    PredictionSchemeForType {
290        ty: draco_oxide_core::attribute::AttributeType,
291        scheme: String,
292    },
293    /// The requested quantization bit count is outside the supported range.
294    #[error("quantization bits {0} out of range (must be 1..=30)")]
295    QuantizationBitsOutOfRange(u8),
296    /// The requested prediction transform is not valid for the attribute type.
297    #[error("prediction transform {transform} is not valid for attribute type {ty:?}")]
298    TransformForType {
299        ty: draco_oxide_core::attribute::AttributeType,
300        transform: String,
301    },
302    /// A prediction transform other than NoTransform was combined with
303    /// NoPrediction, which carries no transform on the wire.
304    #[error("NoPrediction carries no transform on the wire; only NoTransform can accompany it")]
305    TransformWithNoPrediction,
306    /// The selected edgebreaker traversal is not implemented.
307    #[error("the selected edgebreaker traversal is not implemented")]
308    UnsupportedTraversal,
309}
310
311/// Errors returned by the encode entry points.
312#[remain::sorted]
313#[derive(Error, Debug)]
314pub enum Err {
315    /// Attribute encoding failed.
316    #[error("Attribute encoding error: {0}")]
317    AttributeError(#[from] attribute::Err),
318    /// The configuration failed validation.
319    #[error("Invalid encoder configuration: {0}")]
320    ConfigError(#[from] ConfigError),
321    /// Connectivity encoding failed.
322    #[error("Connectivity encoding error: {0}")]
323    ConnectivityError(#[from] connectivity::Err),
324    /// Header encoding failed.
325    #[error("Header encoding error: {0}")]
326    HeaderError(#[from] header::Err),
327    /// Metadata encoding failed.
328    #[error("Metadata encoding error: {0}")]
329    MetadataError(#[from] metadata::Err),
330    /// Point-cloud encoding failed.
331    #[error("Point cloud encoding error: {0}")]
332    PointCloudError(#[from] point_cloud::Err),
333    /// The input mesh has no faces. Encode it with
334    /// [`Encoder::encode_point_cloud`] instead.
335    #[error("the mesh has no faces; encode it as a point cloud instead")]
336    PointCloudInput,
337}
338
339/// The mesh encoder. A single instance is meant to be reused across encodes
340/// so it can share resources between runs.
341#[derive(Default)]
342pub struct Encoder {}
343
344impl Encoder {
345    /// Creates a new encoder.
346    pub fn new() -> Self {
347        Self {}
348    }
349
350    /// Encodes the input mesh into a provided byte stream using the provided configuration.
351    pub fn encode_mesh<W>(&mut self, mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
352    where
353        W: ByteWriter,
354    {
355        encode_impl(mesh, writer, cfg)
356    }
357
358    /// Encodes the input point cloud into a provided byte stream using the
359    /// provided configuration.
360    pub fn encode_point_cloud<W>(
361        &mut self,
362        pc: PointCloud,
363        writer: &mut W,
364        cfg: PointCloudConfig,
365    ) -> Result<(), Err>
366    where
367        W: ByteWriter,
368    {
369        point_cloud::encode_impl(pc, writer, cfg)?;
370        Ok(())
371    }
372}
373
374/// Encodes the input mesh into a provided byte stream using the provided
375/// configuration, with a freshly constructed [`Encoder`].
376pub fn encode_mesh<W>(mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
377where
378    W: ByteWriter,
379{
380    Encoder::new().encode_mesh(mesh, writer, cfg)
381}
382
383/// Encodes the input point cloud into a provided byte stream using the provided
384/// configuration, with a freshly constructed [`Encoder`].
385pub fn encode_point_cloud<W>(
386    pc: PointCloud,
387    writer: &mut W,
388    cfg: PointCloudConfig,
389) -> Result<(), Err>
390where
391    W: ByteWriter,
392{
393    Encoder::new().encode_point_cloud(pc, writer, cfg)
394}
395
396fn encode_impl<W>(mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
397where
398    W: ByteWriter,
399{
400    // Reject inconsistent configs before writing anything.
401    cfg.validate()?;
402
403    // A faceless input has no connectivity to encode; it belongs on the
404    // point-cloud entry point.
405    if mesh.faces.is_empty() {
406        return Err(Err::PointCloudInput);
407    }
408
409    // Encode header
410    header::encode_header(writer, &cfg)?;
411
412    debug_write!("Header done, now starting metadata.", writer);
413
414    // Encode metadata
415    if cfg.metadata {
416        metadata::encode_metadata(&mesh, writer)?;
417    }
418
419    debug_write!("Metadata done, now starting connectivity.", writer);
420
421    // Destruct the mesh so that attributes and faces have the different lifetime.
422    let Mesh {
423        mut attributes,
424        faces,
425        ..
426    } = mesh;
427
428    if !attributes
429        .iter()
430        .any(|att| att.get_attribute_type() == draco_oxide_core::attribute::AttributeType::Position)
431    {
432        return Err(Err::ConnectivityError(
433            connectivity::Err::PositionAttributeTypeError,
434        ));
435    }
436
437    let (ds, pos_corner_table) = ds::build_global_ds(faces, &mut attributes);
438    let mut adss = ds::build_attribute_ds(&ds, &pos_corner_table, attributes);
439
440    // Encode connectivity
441    let corners_of_edgebreaker = connectivity::encode_connectivity(&mut adss, writer, &cfg)?;
442    debug_write!("Connectivity done, now starting attributes.", writer);
443
444    // Encode attributes
445    attribute::encode_attributes(adss, corners_of_edgebreaker, writer, &cfg)?;
446
447    debug_write!("All done", writer);
448
449    Ok(())
450}
451
452#[cfg(test)]
453mod config_tests {
454    use super::*;
455    use draco_oxide_core::attribute::AttributeType;
456    use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType;
457    use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
458
459    #[test]
460    fn default_config_is_valid() {
461        assert!(<Config as ConfigType>::default().validate().is_ok());
462    }
463
464    #[test]
465    fn faceless_mesh_is_rejected_as_point_cloud() {
466        use draco_oxide_core::attribute::{Attribute, AttributeDomain, AttributeType};
467        use draco_oxide_core::types::NdVector;
468        let mut mesh = Mesh::new();
469        mesh.attributes = vec![Attribute::new::<NdVector<3, f32>, 3>(
470            vec![[0.0, 0.0, 0.0].into(), [1.0, 0.0, 0.0].into()],
471            AttributeType::Position,
472            AttributeDomain::Position,
473            Vec::new(),
474        )];
475        let mut out = Vec::new();
476        assert!(matches!(
477            encode_mesh(mesh, &mut out, <Config as ConfigType>::default()),
478            Err(Err::PointCloudInput)
479        ));
480        assert!(out.is_empty(), "nothing must be written before the check");
481    }
482
483    #[test]
484    fn position_quantization_override_validates() {
485        let cfg = Config::default().with_attribute(
486            AttributeType::Position,
487            AttributeConfig {
488                quantization: Some(Quantization::Bits(14)),
489                ..Default::default()
490            },
491        );
492        assert!(cfg.validate().is_ok());
493    }
494
495    #[test]
496    fn texture_predictor_on_normal_is_rejected() {
497        let cfg = Config::default().with_attribute(
498            AttributeType::Normal,
499            AttributeConfig {
500                prediction: Some(PredictionSchemeType::MeshPredictionForTextureCoordinates),
501                ..Default::default()
502            },
503        );
504        assert!(matches!(
505            cfg.validate(),
506            Err(ConfigError::PredictionSchemeForType { .. })
507        ));
508    }
509
510    #[test]
511    fn max_error_quantization_on_normal_is_rejected() {
512        let cfg = Config::default().with_attribute(
513            AttributeType::Normal,
514            AttributeConfig {
515                quantization: Some(Quantization::MaxError(0.01)),
516                ..Default::default()
517            },
518        );
519        assert!(matches!(
520            cfg.validate(),
521            Err(ConfigError::NonBitsQuantizationForNormal)
522        ));
523    }
524
525    #[test]
526    fn normal_encoding_on_position_is_rejected() {
527        let cfg = Config::default().with_attribute(
528            AttributeType::Position,
529            AttributeConfig {
530                normal_encoding: Some(NormalEncoding::PredictedOnly),
531                ..Default::default()
532            },
533        );
534        assert!(matches!(
535            cfg.validate(),
536            Err(ConfigError::NormalEncodingOnNonNormal(
537                AttributeType::Position
538            ))
539        ));
540    }
541
542    #[test]
543    fn out_of_range_bits_is_rejected() {
544        let cfg = Config::default().with_attribute(
545            AttributeType::Position,
546            AttributeConfig {
547                quantization: Some(Quantization::Bits(40)),
548                ..Default::default()
549            },
550        );
551        assert!(matches!(
552            cfg.validate(),
553            Err(ConfigError::QuantizationBitsOutOfRange(40))
554        ));
555    }
556
557    #[test]
558    fn predictive_edgebreaker_is_rejected() {
559        let cfg = Config::default().with_edgebreaker(EdgebreakerConfig {
560            traversal: EdgebreakerKind::Predictive,
561        });
562        assert!(matches!(
563            cfg.validate(),
564            Err(ConfigError::UnsupportedTraversal)
565        ));
566    }
567
568    #[test]
569    fn sequential_selects_sequential_encoder_method() {
570        use draco_oxide_core::codec::header::EncoderMethod;
571        let cfg = Config::default().with_sequential(SequentialConfig::default());
572        assert_eq!(cfg.connectivity.encoder_method(), EncoderMethod::Sequential);
573    }
574}