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
9use draco_oxide_core::bit_coder::ByteWriter;
10use draco_oxide_core::debug_write;
11use draco_oxide_core::mesh::Mesh;
12use draco_oxide_core::types::ConfigType;
13use thiserror::Error;
14
15#[cfg(feature = "evaluation")]
16use crate::eval;
17
18pub trait EncoderConfig {
19    type Encoder;
20    fn get_encoder(&self) -> Self::Encoder;
21}
22
23pub use attribute::{AttributeConfig, NormalEncoding, Quantization};
24pub use connectivity::edgebreaker::Config as EdgebreakerConfig;
25pub use connectivity::sequential::Config as SequentialConfig;
26pub use connectivity::Config as ConnectivityConfig;
27
28use config_spec::ConfigSpec;
29
30#[derive(Debug, Clone, serde::Deserialize)]
31#[serde(from = "ConfigSpec")]
32pub struct Config {
33    // Connectivity compression method and its config (edgebreaker or sequential).
34    // Also the single source of truth for the header's encoder method.
35    connectivity: connectivity::Config,
36    // Per-attribute encoding configuration (see `attribute::Config`).
37    attribute: attribute::Config,
38    geometry_type: header::EncodedGeometryType,
39    metadata: bool,
40}
41
42impl ConfigType for Config {
43    fn default() -> Self {
44        Self {
45            connectivity: connectivity::Config::default(),
46            attribute: attribute::Config::default(),
47            geometry_type: header::EncodedGeometryType::TrianglarMesh,
48            metadata: false,
49        }
50    }
51}
52
53impl Config {
54    /// Sets how normal attributes are compressed.
55    ///
56    /// [`NormalEncoding::PredictedOnly`] makes normal compression effectively
57    /// zero-CPU: the encoder ignores the input normal values (using only their
58    /// seam topology) and emits an all-zero correction stream, so the decoder
59    /// reconstructs exactly the normals it predicts from the geometry.
60    ///
61    /// ```no_run
62    /// # use draco_oxide::core::types::ConfigType;
63    /// use draco_oxide::encode::{Config, NormalEncoding};
64    /// let cfg = Config::default().with_normals(NormalEncoding::PredictedOnly);
65    /// ```
66    pub fn with_normals(mut self, enc: NormalEncoding) -> Self {
67        self.attribute.set_normal_encoding(enc);
68        self
69    }
70
71    /// Overrides the per-type encoding for `ty` (prediction scheme, transform,
72    /// quantization, and — for normals — the normal encoding mode). Absent knobs
73    /// fall back to the built-in default for that attribute type.
74    pub fn with_attribute(
75        mut self,
76        ty: draco_oxide_core::attribute::AttributeType,
77        cfg: AttributeConfig,
78    ) -> Self {
79        self.attribute.set(ty, cfg);
80        self
81    }
82
83    /// Selects the connectivity compression method and its configuration.
84    pub fn with_connectivity(mut self, cfg: ConnectivityConfig) -> Self {
85        self.connectivity = cfg;
86        self
87    }
88
89    /// Selects edgebreaker connectivity compression with the given config.
90    pub fn with_edgebreaker(mut self, cfg: EdgebreakerConfig) -> Self {
91        self.connectivity = ConnectivityConfig::Edgebreaker(cfg);
92        self
93    }
94
95    /// Selects sequential connectivity compression with the given config.
96    pub fn with_sequential(mut self, cfg: SequentialConfig) -> Self {
97        self.connectivity = ConnectivityConfig::Sequential(cfg);
98        self
99    }
100
101    /// Enables or disables metadata encoding.
102    pub fn with_metadata(mut self, metadata: bool) -> Self {
103        self.metadata = metadata;
104        self
105    }
106
107    /// The current per-type attribute override for `ty` (empty default if none),
108    /// for read-modify-write layering of overrides (e.g. a CLI flag patching a
109    /// single knob on top of a file-loaded config).
110    pub fn attribute_config(
111        &self,
112        ty: draco_oxide_core::attribute::AttributeType,
113    ) -> AttributeConfig {
114        self.attribute.get(ty)
115    }
116
117    /// The selected connectivity configuration.
118    pub fn connectivity(&self) -> &ConnectivityConfig {
119        &self.connectivity
120    }
121
122    /// Validates the configuration for internal consistency, rejecting
123    /// combinations that would produce an undecodable or nonsensical stream (a
124    /// texture predictor on a normal attribute, a coordinate max-error on an
125    /// octahedral normal, an unimplemented traversal, out-of-range bits, …).
126    /// Called automatically at the top of [`encode`].
127    pub fn validate(&self) -> Result<(), ConfigError> {
128        use draco_oxide_core::attribute::AttributeType;
129        use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
130
131        // Connectivity: reject the unimplemented predictive edgebreaker traversal.
132        if let ConnectivityConfig::Edgebreaker(eb) = &self.connectivity {
133            if eb.traversal == EdgebreakerKind::Predictive {
134                return Err(ConfigError::UnsupportedTraversal);
135            }
136        }
137
138        for (&ty, over) in self.attribute.overrides() {
139            if over.normal_encoding.is_some() && ty != AttributeType::Normal {
140                return Err(ConfigError::NormalEncodingOnNonNormal(ty));
141            }
142
143            if let Some(scheme) = &over.prediction {
144                if !allowed_schemes(ty).iter().any(|s| s == scheme) {
145                    return Err(ConfigError::PredictionSchemeForType {
146                        ty,
147                        scheme: scheme.to_string(),
148                    });
149                }
150            }
151
152            if let Some(transform) = over.transform {
153                if !allowed_transforms(ty).contains(&transform) {
154                    return Err(ConfigError::TransformForType {
155                        ty,
156                        transform: format!("{transform:?}"),
157                    });
158                }
159            }
160
161            if let Some(quant) = over.quantization {
162                // Octahedral (normal) resolution is angular, not coordinate; only
163                // an explicit bit count is meaningful there.
164                if ty == AttributeType::Normal && !matches!(quant, Quantization::Bits(_)) {
165                    return Err(ConfigError::NonBitsQuantizationForNormal);
166                }
167                if let Quantization::Bits(n) = quant {
168                    if !(1..=30).contains(&n) {
169                        return Err(ConfigError::QuantizationBitsOutOfRange(n));
170                    }
171                }
172            }
173        }
174
175        Ok(())
176    }
177}
178
179/// The prediction schemes valid for a given attribute type.
180fn allowed_schemes(
181    ty: draco_oxide_core::attribute::AttributeType,
182) -> Vec<draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType> {
183    use draco_oxide_core::attribute::AttributeType::*;
184    use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType as S;
185    match ty {
186        Position => vec![
187            S::MeshParallelogramPrediction,
188            S::MeshMultiParallelogramPrediction,
189            S::DeltaPrediction,
190            S::NoPrediction,
191        ],
192        Normal => vec![S::MeshNormalPrediction],
193        TextureCoordinate => vec![
194            S::MeshPredictionForTextureCoordinates,
195            S::DerivativePrediction,
196            S::DeltaPrediction,
197            S::NoPrediction,
198        ],
199        // Color, Custom, and any other generic per-vertex attribute have no
200        // mesh-geometry predictor.
201        _ => vec![S::DeltaPrediction, S::NoPrediction],
202    }
203}
204
205/// The prediction transforms valid for a given attribute type.
206fn allowed_transforms(
207    ty: draco_oxide_core::attribute::AttributeType,
208) -> Vec<attribute::PredictionTransformType> {
209    use attribute::PredictionTransformType as T;
210    use draco_oxide_core::attribute::AttributeType::*;
211    match ty {
212        // Normals ride the octahedral transforms.
213        Normal => vec![T::OctahedralOrthogonal, T::OctahedralReflection],
214        _ => vec![T::Difference, T::WrappedDifference, T::NoTransform],
215    }
216}
217
218/// Errors from [`Config::validate`].
219#[remain::sorted]
220#[derive(Error, Debug)]
221pub enum ConfigError {
222    #[error("normals accept only an explicit bit count (octahedral error is angular)")]
223    NonBitsQuantizationForNormal,
224    #[error("normal encoding was set on a non-normal attribute ({0:?})")]
225    NormalEncodingOnNonNormal(draco_oxide_core::attribute::AttributeType),
226    #[error("prediction scheme {scheme} is not valid for attribute type {ty:?}")]
227    PredictionSchemeForType {
228        ty: draco_oxide_core::attribute::AttributeType,
229        scheme: String,
230    },
231    #[error("quantization bits {0} out of range (must be 1..=30)")]
232    QuantizationBitsOutOfRange(u8),
233    #[error("prediction transform {transform} is not valid for attribute type {ty:?}")]
234    TransformForType {
235        ty: draco_oxide_core::attribute::AttributeType,
236        transform: String,
237    },
238    #[error("the selected edgebreaker traversal is not implemented")]
239    UnsupportedTraversal,
240}
241
242#[remain::sorted]
243#[derive(Error, Debug)]
244pub enum Err {
245    #[error("Attribute encoding error: {0}")]
246    AttributeError(#[from] attribute::Err),
247    #[error("Invalid encoder configuration: {0}")]
248    ConfigError(#[from] ConfigError),
249    #[error("Connectivity encoding error: {0}")]
250    ConnectivityError(#[from] connectivity::Err),
251    #[error("Header encoding error: {0}")]
252    HeaderError(#[from] header::Err),
253    #[error("Metadata encoding error: {0}")]
254    MetadataError(#[from] metadata::Err),
255}
256
257/// Encodes the input mesh into a provided byte stream using the provided configuration.
258pub fn encode<W>(mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
259where
260    W: ByteWriter,
261{
262    // Reject inconsistent configs before writing anything.
263    cfg.validate()?;
264
265    #[cfg(feature = "evaluation")]
266    eval::scope_begin("compression info", writer);
267
268    // Encode header
269    header::encode_header(writer, &cfg)?;
270
271    debug_write!("Header done, now starting metadata.", writer);
272
273    // Encode metadata
274    if cfg.metadata {
275        #[cfg(feature = "evaluation")]
276        eval::scope_begin("metadata", writer);
277        metadata::encode_metadata(&mesh, writer)?;
278        #[cfg(feature = "evaluation")]
279        eval::scope_end(writer);
280    }
281
282    debug_write!("Metadata done, now starting connectivity.", writer);
283
284    // Destruct the mesh so that attributes and faces have the different lifetime.
285    let Mesh {
286        mut attributes,
287        faces,
288        ..
289    } = mesh;
290
291    if !attributes
292        .iter()
293        .any(|att| att.get_attribute_type() == draco_oxide_core::attribute::AttributeType::Position)
294    {
295        return Err(Err::ConnectivityError(
296            connectivity::Err::PositionAttributeTypeError,
297        ));
298    }
299
300    let (ds, pos_corner_table) = ds::build_global_ds(faces, &mut attributes);
301    let mut adss = ds::build_attribute_ds(&ds, &pos_corner_table, attributes);
302
303    // Encode connectivity
304    let corners_of_edgebreaker = connectivity::encode_connectivity(&mut adss, writer, &cfg)?;
305    debug_write!("Connectivity done, now starting attributes.", writer);
306
307    // Encode attributes
308    attribute::encode_attributes(adss, corners_of_edgebreaker, writer, &cfg)?;
309
310    debug_write!("All done", writer);
311
312    #[cfg(feature = "evaluation")]
313    eval::scope_end(writer);
314    Ok(())
315}
316
317#[cfg(test)]
318mod config_tests {
319    use super::*;
320    use draco_oxide_core::attribute::AttributeType;
321    use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType;
322    use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
323
324    #[test]
325    fn default_config_is_valid() {
326        assert!(<Config as ConfigType>::default().validate().is_ok());
327    }
328
329    #[test]
330    fn position_quantization_override_validates() {
331        let cfg = Config::default().with_attribute(
332            AttributeType::Position,
333            AttributeConfig {
334                quantization: Some(Quantization::Bits(14)),
335                ..Default::default()
336            },
337        );
338        assert!(cfg.validate().is_ok());
339    }
340
341    #[test]
342    fn texture_predictor_on_normal_is_rejected() {
343        let cfg = Config::default().with_attribute(
344            AttributeType::Normal,
345            AttributeConfig {
346                prediction: Some(PredictionSchemeType::MeshPredictionForTextureCoordinates),
347                ..Default::default()
348            },
349        );
350        assert!(matches!(
351            cfg.validate(),
352            Err(ConfigError::PredictionSchemeForType { .. })
353        ));
354    }
355
356    #[test]
357    fn max_error_quantization_on_normal_is_rejected() {
358        let cfg = Config::default().with_attribute(
359            AttributeType::Normal,
360            AttributeConfig {
361                quantization: Some(Quantization::MaxError(0.01)),
362                ..Default::default()
363            },
364        );
365        assert!(matches!(
366            cfg.validate(),
367            Err(ConfigError::NonBitsQuantizationForNormal)
368        ));
369    }
370
371    #[test]
372    fn normal_encoding_on_position_is_rejected() {
373        let cfg = Config::default().with_attribute(
374            AttributeType::Position,
375            AttributeConfig {
376                normal_encoding: Some(NormalEncoding::PredictedOnly),
377                ..Default::default()
378            },
379        );
380        assert!(matches!(
381            cfg.validate(),
382            Err(ConfigError::NormalEncodingOnNonNormal(
383                AttributeType::Position
384            ))
385        ));
386    }
387
388    #[test]
389    fn out_of_range_bits_is_rejected() {
390        let cfg = Config::default().with_attribute(
391            AttributeType::Position,
392            AttributeConfig {
393                quantization: Some(Quantization::Bits(40)),
394                ..Default::default()
395            },
396        );
397        assert!(matches!(
398            cfg.validate(),
399            Err(ConfigError::QuantizationBitsOutOfRange(40))
400        ));
401    }
402
403    #[test]
404    fn predictive_edgebreaker_is_rejected() {
405        let cfg = Config::default().with_edgebreaker(EdgebreakerConfig {
406            traversal: EdgebreakerKind::Predictive,
407            use_single_connectivity: false,
408        });
409        assert!(matches!(
410            cfg.validate(),
411            Err(ConfigError::UnsupportedTraversal)
412        ));
413    }
414
415    #[test]
416    fn sequential_selects_sequential_encoder_method() {
417        use draco_oxide_core::codec::header::EncoderMethod;
418        let cfg = Config::default().with_sequential(SequentialConfig::default());
419        assert_eq!(cfg.connectivity.encoder_method(), EncoderMethod::Sequential);
420    }
421}