1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
use std::collections::HashMap;
use std::sync::Arc;
/// Integer option bag used to configure Draco encoding.
///
/// Options mirror the C++ Draco encoder style: global options apply to the
/// whole geometry, while attribute options override a value for one attribute
/// id and fall back to the global value when unset.
///
/// Common keys include `quantization_bits` (per-attribute precision),
/// `encoding_speed`/`decoding_speed`, `encoding_method`, and
/// `prediction_scheme`. Keys without an explicit setter are read and written
/// with [`get_global_int`](EncoderOptions::get_global_int) /
/// [`set_global_int`](EncoderOptions::set_global_int).
///
/// # Examples
///
/// ```
/// use draco_core::EncoderOptions;
///
/// let mut options = EncoderOptions::new();
/// options.set_global_int("quantization_bits", 14); // default for all attributes
/// options.set_attribute_int(0, "quantization_bits", 10); // override attribute 0
///
/// assert_eq!(options.get_attribute_int(0, "quantization_bits", 0), 10);
/// // Attribute 1 has no override, so it falls back to the global value.
/// assert_eq!(options.get_attribute_int(1, "quantization_bits", 0), 14);
/// ```
#[derive(Debug, Clone, Default)]
pub struct EncoderOptions {
inner: Arc<Inner>,
}
/// The option maps, shared until a setter needs its own copy.
///
/// The encoders clone the whole bag on every `encode` call and keep the copy
/// for the duration. A deep clone paid one allocation per map and per `String`
/// key each time -- pure bookkeeping, and on a tiny mesh a visible share of
/// the per-call floor. Behind an `Arc` the clone is a reference count, and a
/// setter on a bag that is shared at that moment copies the maps once for
/// itself (`Arc::make_mut`); a bag nobody else holds is mutated in place.
#[derive(Debug, Clone, Default)]
struct Inner {
global_options: HashMap<String, i32>,
attribute_options: HashMap<i32, HashMap<String, i32>>,
}
impl EncoderOptions {
/// Creates options with Draco-compatible defaults.
pub fn new() -> Self {
Self::default()
}
/// Returns the configured encoding speed, defaulting to 5.
pub fn get_encoding_speed(&self) -> i32 {
self.get_global_int("encoding_speed", 5)
}
/// Returns the configured decoding speed target, defaulting to 5.
pub fn get_decoding_speed(&self) -> i32 {
self.get_global_int("decoding_speed", 5)
}
/// Returns the maximum speed for both encoding/decoding.
/// Matches C++ ExpertEncoder::GetSpeed() behavior.
pub fn get_speed(&self) -> i32 {
let encoding_speed = self
.inner
.global_options
.get("encoding_speed")
.copied()
.unwrap_or(-1);
let decoding_speed = self
.inner
.global_options
.get("decoding_speed")
.copied()
.unwrap_or(-1);
let max_speed = encoding_speed.max(decoding_speed);
if max_speed == -1 {
5 // Default value
} else {
max_speed
}
}
/// Sets both speeds from a `draco_encoder`-style compression level.
///
/// The CLI's `-cl` runs 0 (least compression) to 10 (most), the opposite
/// sense of `encoding_speed`/`decoding_speed`, and converts with
/// `speed = 10 - compression_level` before calling the same
/// `SetSpeedOptions` this crate mirrors; this method does the same
/// conversion and nothing else. `level` is not range-checked, matching
/// the CLI, which passes an out-of-range `-cl` straight through the same
/// subtraction rather than rejecting it.
pub fn set_compression_level(&mut self, level: i32) {
let speed = 10 - level;
self.set_global_int("encoding_speed", speed);
self.set_global_int("decoding_speed", speed);
}
/// Returns `10 - `[`get_speed`](Self::get_speed)`()`, the CLI's compression
/// level for the speed this instance currently carries.
///
/// A fresh `EncoderOptions` reports 5 here, not the CLI's own default of
/// 7 — the two tools default to different speeds (5 here, 3 there), and
/// this getter reads what is actually set rather than what the CLI would
/// have chosen.
pub fn get_compression_level(&self) -> i32 {
10 - self.get_speed()
}
/// Sets `quantization_bits` for one attribute id.
///
/// Equivalent to `ExpertEncoder::SetAttributeQuantization` and to what the
/// CLI's `-qp`/`-qt`/`-qn`/`-qg` resolve to once they have picked an
/// attribute id for POSITION/TEX_COORD/NORMAL/GENERIC; this method takes
/// the id directly rather than a geometry attribute type.
pub fn set_attribute_quantization(&mut self, att_id: i32, quantization_bits: i32) {
self.set_attribute_int(att_id, "quantization_bits", quantization_bits);
}
/// Returns the `quantization_bits` set for one attribute id, or -1 if
/// none was set for it or globally.
pub fn get_attribute_quantization(&self, att_id: i32) -> i32 {
self.get_attribute_int(att_id, "quantization_bits", -1)
}
/// Returns the forced prediction scheme, or -1 for the encoder default.
pub fn get_prediction_scheme(&self) -> i32 {
self.get_global_int("prediction_scheme", -1)
}
/// Returns the prediction scheme forced for one attribute, falling back to
/// the global setting and then to -1 for the encoder default.
///
/// Upstream reads this per attribute (`GetPredictionMethodFromOptions`), and
/// `get_attribute_int` itself falls back to the global option, so a value
/// set either way is honoured. Crate-internal: callers outside can already
/// express both halves with `set_attribute_int` and `set_prediction_scheme`,
/// and a getter with no matching per-attribute setter would be a half API.
pub(crate) fn get_attribute_prediction_scheme(&self, att_id: i32) -> i32 {
self.get_attribute_int(att_id, "prediction_scheme", -1)
}
/// Forces a prediction scheme by numeric Draco method id.
pub fn set_prediction_scheme(&mut self, value: i32) {
self.set_global_int("prediction_scheme", value);
}
/// Whether the encoder may choose a point-cloud attribute's prediction
/// scheme by estimating the cost of each candidate.
pub fn prediction_search(&self) -> bool {
self.get_global_int("prediction_scheme_search", 0) != 0
}
/// Lets the encoder choose each attribute's prediction scheme by the
/// estimated cost of the candidates rather than by upstream's fixed rule.
///
/// Off by default, and deliberately: the automatic choice is upstream's,
/// and this crate's output is byte-identical to C++ Draco's for the same
/// input. Searching produces a different — smaller — stream, so it is a
/// thing a caller asks for rather than a thing that happens to them.
///
/// What it buys, and why it exists at all: the automatic choice for a
/// point-cloud attribute is always `Difference`, and differencing costs
/// more than it saves whenever consecutive values do not correlate.
/// Spherical-harmonic coefficients in a Gaussian splat are the case that
/// prompted this: predicted, they cost more bits than coded directly. The
/// opposite case is just as real: on smoothly varying data, turning
/// prediction off makes a file much larger. Neither is knowable from the
/// attribute's type, so this looks at its values.
///
/// **It is worth turning on only for data of that shape**, and the honest
/// version of "that shape" is narrow. On a photogrammetry capture carrying
/// position and colour the search finds nothing at all: both attributes
/// are well served by differencing, and coding either directly is worse.
/// Attributes whose values do not follow their neighbours are what this is
/// for, and a scanned surface is the opposite of that.
///
/// The cost is encode time and nothing else. The candidates are ranked by
/// the same bit estimate the symbol coder uses to choose its own scheme,
/// which is an entropy pass over each candidate's symbols, not a second
/// encode, and the winner's estimate is what the coder is then handed
/// rather than working it out again. On a cloud of many attributes the
/// encode still takes noticeably longer. Decoding is unaffected, and every stream this can produce is one an ordinary decoder
/// reads: the scheme is a byte the bitstream has always carried,
/// `PREDICTION_NONE` included.
///
/// An attribute with an explicit `prediction_scheme` is left alone; a
/// caller who named a scheme has already made this choice.
pub fn set_prediction_search(&mut self, enabled: bool) {
self.set_global_int("prediction_scheme_search", i32::from(enabled));
}
/// Whether a point cloud's points may be reordered spatially before being
/// encoded.
pub fn spatial_point_order(&self) -> bool {
self.get_global_int("spatial_point_order", 0) != 0
}
/// Lets the encoder emit a point cloud's points in a spatial order rather
/// than in the order they were handed in.
///
/// Which spatial order is the encoder's choice and not part of this
/// option's contract: today it is a Hilbert curve, which writes fewer bytes
/// than a Morton curve for the same encode time, and a later version may
/// use a better one. Every such stream decodes the
/// same way; only the order of the decoded points and the size differ.
///
/// A point cloud's point order carries no meaning: no connectivity refers
/// to it, every attribute is read through the same point index, and a
/// decoder reconstructs whatever order the stream has. So an encoder may
/// choose it, and choosing it spatially is what makes the difference
/// predictor predict from a neighbour instead of from whatever the
/// exporter happened to write next.
///
/// **This is the general one of the two.** It was written for Gaussian
/// splats, and it does more on ordinary captured geometry: a
/// photogrammetry point cloud of coloured points gains a larger share than
/// a splat does. Any cloud whose attributes vary through space rather than
/// along its file order gains from it.
///
/// The curve is laid over a grid as fine as the positions' own
/// `quantization_bits`, up to 21 bits an axis. Both halves of that are
/// measured: a coarser grid puts points the stream will distinguish into
/// one cell, where their order is whatever the sort left them in, and a
/// finer one sorts by differences the quantization discards.
///
/// Off by default for the same reason the prediction search is: the output
/// differs, byte for byte, from what upstream C++ Draco writes for the same
/// input, and this crate's default is to match it.
///
/// **This reorders the decoded points.** Anything outside the file that
/// indexes into it by point number — a sidecar array, an index written by
/// another tool — will be pointing at different points afterwards. Nothing
/// inside a `.drc` does, which is why this is expressible at all, but a
/// caller who has such a thing is the one who knows.
///
/// **It can also make a file bigger**, and unlike the prediction search it
/// does not check. The gain comes from attributes that vary through space;
/// an attribute that varies along the order it was handed in — an index, a
/// timestamp, anything written in sequence — is scrambled by the reorder
/// and costs more afterwards: a cloud of positions plus a running integer
/// tag grows. It is not checked because the option is a
/// statement about the order, not about the size: a caller who wants
/// spatial locality in the decoded cloud wants it whether or not it also
/// happens to compress better. Whoever wants only the smaller file wants
/// [`set_point_order_search`](Self::set_point_order_search) instead, which
/// checks, and is allowed to trade locality for size.
///
/// Applies to the sequential coder. The kd-tree coder chooses its own point
/// order and this leaves it alone. A point cloud with no position attribute
/// has nothing to sort by and is also left alone.
pub fn set_spatial_point_order(&mut self, enabled: bool) {
self.set_global_int("spatial_point_order", i32::from(enabled));
}
/// Whether the encoder may search for the order of a point cloud's points
/// that makes the stream smallest.
pub fn point_order_search(&self) -> bool {
self.get_global_int("point_order_search", 0) != 0
}
/// Lets the encoder search for the order of a point cloud's points that
/// makes the stream smallest, and leave the order alone where it finds the
/// one it was handed already better.
///
/// The sequential coder writes each attribute as the difference between a
/// point and the one before it, so the order decides how large those
/// differences are. A spatial order makes neighbours in space neighbours
/// in the stream; this strings the points along a Hilbert curve and then
/// repairs it where the other attributes disagree with the positions,
/// which they do in a Gaussian splat, where the position is a small part
/// of each point's bytes.
///
/// Against [`set_spatial_point_order`](Self::set_spatial_point_order) at
/// the same encoding speed it gains the most on clouds like that, and more
/// the lower the speed. Photogrammetry scans and lidar captures gain less.
/// The stream is an ordinary one. Only the order of the points in it
/// differs.
///
/// **It costs encode time**, several times the encode in file order, and
/// more the lower the speed. The search runs on as many threads as
/// [`set_threads`](Self::set_threads) allows, writing the same order on any
/// number. **And some decode time**: in the order it finds, neighbours
/// agree well enough that the encoder picks the more compact schemes,
/// differences over values as they are and the raw coder over the tagged
/// one, and those take a little longer to undo.
///
/// **It can decline.** Each order it could write -- the one it was handed,
/// the curve, and the curve repaired -- is priced by the symbol coder's own
/// plan of what it would write for a sample of the cloud, and an order
/// replaces another only if it undercuts it by 1%. The repair is tried on a
/// sample of the path's blocks first, one in sixteen on a large cloud, and
/// finished only if the trial pays. A scan or a rotating-lidar capture
/// written along its scan lines is already in an order no curve improves
/// on, and comes out exactly as it would without this, where a spatial
/// order would have made it larger.
///
/// **The effort follows `encoding_speed`**, the way it does elsewhere:
/// speed 9 and 10 write the curve and nothing more, 7 and 8 count the 8
/// dearest attribute columns over a window of 8 points, 5 and 6 (the
/// default) 16 over 16, 3 and 4 32 over 32, and 0 to 2 the 64 dearest over
/// 64.
///
/// Off by default for the reason the prediction search is: it changes the
/// bytes, and this crate's default is to match upstream C++ Draco.
///
/// **This reorders the decoded points**, and unlike
/// [`set_spatial_point_order`](Self::set_spatial_point_order) it makes no
/// promise that they are spatially near one another: a point may sit
/// beside one that looks like it rather than one that is close. If the
/// search keeps the order it was handed and `set_spatial_point_order` is
/// also on, the spatial order is what is written.
///
/// Applies to the sequential coder, like the order option does.
pub fn set_point_order_search(&mut self, enabled: bool) {
self.set_global_int("point_order_search", i32::from(enabled));
}
/// The threads asked of this encode: 1, the default, is the calling thread
/// alone, 0 is as many as the machine has, and no count is taken past
/// sixteen.
pub fn get_threads(&self) -> i32 {
self.get_global_int("threads", 1)
}
/// Caps the threads an encode may use; `1`, the default, keeps everything
/// on the calling thread and `0` takes as many as the machine has. Neither
/// a count asked for nor the machine's is taken past sixteen.
///
/// One by default because a library cannot see what its caller already
/// runs: a server encoding on a pool of its own, or a converter working
/// through files side by side, would have every encode multiply its
/// threads by up to sixteen. A caller with the cores to spare, encoding a
/// large cloud at a time, asks for them.
///
/// A sequential point cloud's attributes are encoded side by side, each
/// into a buffer of its own that is appended in order, and an attribute of
/// a million values or more also cuts its own passes -- quantization, the
/// gather, the prediction, the symbol plan -- into pieces; only the rANS
/// write stays one chain. The point order search and the spatial curve run
/// on the same threads. A large cloud of many attributes, a Gaussian splat
/// for one, encodes several times faster on many threads. Below 2^17 values
/// in all, counted as points times attributes, no thread is started.
///
/// Only work that splits into pieces independent of one another is run on
/// threads, and each piece's result is the same whichever thread works it,
/// so the stream does not depend on this. The calling thread is one of
/// them, and a thread the system refuses to start is one fewer rather than
/// an error: the encode finishes on those that did. On WebAssembly there
/// are no threads and the value is ignored.
pub fn set_threads(&mut self, threads: i32) {
self.set_global_int("threads", threads);
}
/// Returns the forced encoding method, if one was set.
pub fn get_encoding_method(&self) -> Option<i32> {
self.inner.global_options.get("encoding_method").cloned()
}
/// Forces an encoding method by numeric Draco method id.
pub fn set_encoding_method(&mut self, value: i32) {
self.set_global_int("encoding_method", value);
}
/// Sets the target Draco bitstream version.
pub fn set_version(&mut self, major: u8, minor: u8) {
self.set_global_int("version_major", major as i32);
self.set_global_int("version_minor", minor as i32);
}
/// Returns the target Draco bitstream version, or `(0, 0)` for default.
pub fn get_version(&self) -> (u8, u8) {
let major = self.get_global_int("version_major", -1);
let minor = self.get_global_int("version_minor", -1);
if major == -1 || minor == -1 {
// Default version depends on the encoder type and method,
// but we'll return (0, 0) to indicate "use default".
(0, 0)
} else {
(major as u8, minor as u8)
}
}
/// Sets a global integer option.
pub fn set_global_int(&mut self, key: &str, value: i32) {
Arc::make_mut(&mut self.inner)
.global_options
.insert(key.to_string(), value);
}
/// Returns a global integer option or the supplied default.
pub fn get_global_int(&self, key: &str, default_val: i32) -> i32 {
*self.inner.global_options.get(key).unwrap_or(&default_val)
}
/// Sets an integer option for one attribute id.
pub fn set_attribute_int(&mut self, att_id: i32, key: &str, value: i32) {
Arc::make_mut(&mut self.inner)
.attribute_options
.entry(att_id)
.or_default()
.insert(key.to_string(), value);
}
/// Returns an attribute integer option, falling back to the global value.
pub fn get_attribute_int(&self, att_id: i32, key: &str, default_val: i32) -> i32 {
if let Some(opts) = self.inner.attribute_options.get(&att_id) {
if let Some(val) = opts.get(key) {
return *val;
}
}
// Falls back to the global value, as upstream does.
self.get_global_int(key, default_val)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `draco_encoder`'s own default is `-cl 7`; this crate's is speed 5.
/// Verified against the CLI: `EncoderOptions::new()` and
/// `draco_encoder -cl 5 -qp 0` produce byte-identical output.
#[test]
fn compression_level_defaults_to_5_not_the_cli_default_of_7() {
let options = EncoderOptions::new();
assert_eq!(options.get_compression_level(), 5);
}
/// `speed = 10 - compression_level`, matching `draco_encoder.cc`'s own
/// conversion before it calls `SetSpeedOptions`.
#[test]
fn set_compression_level_matches_the_cli_conversion() {
let mut options = EncoderOptions::new();
options.set_compression_level(7);
assert_eq!(options.get_speed(), 3);
assert_eq!(options.get_encoding_speed(), 3);
assert_eq!(options.get_decoding_speed(), 3);
assert_eq!(options.get_compression_level(), 7);
}
#[test]
fn set_compression_level_round_trips_the_full_cli_range() {
for level in 0..=10 {
let mut options = EncoderOptions::new();
options.set_compression_level(level);
assert_eq!(options.get_compression_level(), level);
}
}
/// A clone shares the maps, and a setter on either side afterwards
/// changes only the side it was called on.
#[test]
fn a_clone_shares_the_maps_until_one_side_writes() {
let mut options = EncoderOptions::new();
options.set_global_int("quantization_bits", 14);
options.set_attribute_int(1, "quantization_bits", 10);
let mut cloned = options.clone();
assert!(Arc::ptr_eq(&options.inner, &cloned.inner));
cloned.set_attribute_int(1, "quantization_bits", 8);
options.set_global_int("quantization_bits", 12);
assert!(!Arc::ptr_eq(&options.inner, &cloned.inner));
assert_eq!(options.get_attribute_int(1, "quantization_bits", 0), 10);
assert_eq!(options.get_global_int("quantization_bits", 0), 12);
assert_eq!(cloned.get_attribute_int(1, "quantization_bits", 0), 8);
assert_eq!(cloned.get_global_int("quantization_bits", 0), 14);
}
#[test]
fn attribute_quantization_defaults_to_unset() {
let options = EncoderOptions::new();
assert_eq!(options.get_attribute_quantization(0), -1);
}
#[test]
fn set_attribute_quantization_round_trips() {
let mut options = EncoderOptions::new();
options.set_attribute_quantization(0, 14);
options.set_attribute_quantization(1, 10);
assert_eq!(options.get_attribute_quantization(0), 14);
assert_eq!(options.get_attribute_quantization(1), 10);
// Unset attributes fall back to the global value, same as
// `get_attribute_int` -- there is none here, so -1.
assert_eq!(options.get_attribute_quantization(2), -1);
}
}