1#[cfg(any(nvidia_omm_native, test))]
2mod ffi;
3
4#[cfg(nvidia_omm_native)]
5mod native;
6
7#[cfg(not(nvidia_omm_native))]
8mod stub;
9
10#[cfg(nvidia_omm_native)]
11pub use native::{AlphaTexture, Baker};
12
13#[cfg(not(nvidia_omm_native))]
14pub use stub::{AlphaTexture, Baker};
15
16#[cfg(any(nvidia_omm_native, test))]
17use anyhow::{Context as _, Result, ensure};
18
19pub const ALPHA_CUTOFF: f32 = 0.5;
20pub const FOUR_STATE_FORMAT: u16 = 2;
21pub const SDK_VERSION: &str = "1.9.2";
22
23#[derive(Clone, Copy, Debug)]
24pub struct AlphaMip<'a> {
25 pub width: u32,
27 pub height: u32,
29 pub row_pitch: u32,
31 pub data: &'a [u8],
32}
33
34#[cfg(any(nvidia_omm_native, test))]
35impl AlphaMip<'_> {
36 fn validate_all(mips: &[Self]) -> Result<()> {
37 ensure!(
38 !mips.is_empty(),
39 "nvidia omm texture requires at least one mip"
40 );
41 ensure!(
42 mips.windows(2)
43 .all(|pair| pair[1].width <= pair[0].width && pair[1].height <= pair[0].height),
44 "nvidia omm mip dimensions must be non-increasing"
45 );
46
47 for mip in mips {
48 ensure!(
49 mip.width > 0 && mip.height > 0,
50 "nvidia omm mip extent must be nonzero"
51 );
52
53 let row_pitch = if mip.row_pitch == 0 {
54 mip.width
55 } else {
56 mip.row_pitch
57 };
58
59 ensure!(
60 row_pitch >= mip.width,
61 "nvidia omm mip row pitch is smaller than its width"
62 );
63
64 let pitch = usize::try_from(row_pitch).context("nvidia omm row pitch overflow")?;
65 let preceding_rows =
66 usize::try_from(mip.height - 1).context("nvidia omm mip height overflow")?;
67 let width = usize::try_from(mip.width).context("nvidia omm mip width overflow")?;
68 let required = preceding_rows
69 .checked_mul(pitch)
70 .and_then(|offset| offset.checked_add(width))
71 .context("nvidia omm mip byte size overflow")?;
72
73 ensure!(
74 mip.data.len() >= required,
75 "nvidia omm mip data is truncated"
76 );
77 }
78
79 Ok(())
80 }
81}
82
83#[derive(Clone, Copy, Debug, PartialEq)]
84pub struct BakeConfig {
85 pub dynamic_subdivision_scale: f32,
88 pub rejection_threshold: f32,
89 pub max_subdivision_level: u8,
90 pub max_array_data_size: u32,
91 pub max_workload_size: u64,
92 pub internal_threads: bool,
93 pub validation: bool,
94}
95
96impl Default for BakeConfig {
97 fn default() -> Self {
98 Self {
99 dynamic_subdivision_scale: 2.0,
100 rejection_threshold: 0.0,
101 max_subdivision_level: 8,
102 max_array_data_size: u32::MAX,
103 max_workload_size: u64::MAX,
104 internal_threads: true,
105 validation: cfg!(debug_assertions),
106 }
107 }
108}
109
110#[derive(Clone, Copy, Debug)]
111pub struct BakeInput<'a> {
112 pub texture_coordinates: &'a [[f32; 2]],
117 pub indices: &'a [u32],
118 pub config: BakeConfig,
119}
120
121#[cfg(any(nvidia_omm_native, test))]
122impl BakeInput<'_> {
123 fn validate(self, texture_size: [u32; 2]) -> Result<()> {
124 ensure!(
125 !self.indices.is_empty() && self.indices.len().is_multiple_of(3),
126 "nvidia omm indices must contain complete triangles"
127 );
128 ensure!(
129 !self.texture_coordinates.is_empty(),
130 "nvidia omm texture coordinates must not be empty"
131 );
132 ensure!(
133 self.indices
134 .iter()
135 .all(|&index| (index as usize) < self.texture_coordinates.len()),
136 "nvidia omm index exceeds the texture-coordinate count"
137 );
138 ensure!(
139 self.config.dynamic_subdivision_scale.is_finite()
140 && self.config.dynamic_subdivision_scale > 0.0,
141 "nvidia omm dynamic subdivision scale must be finite and positive"
142 );
143 ensure!(
144 (0.0..=1.0).contains(&self.config.rejection_threshold),
145 "nvidia omm rejection threshold must be in [0, 1]"
146 );
147 ensure!(
148 self.config.max_subdivision_level <= 12,
149 "nvidia omm subdivision level exceeds 12"
150 );
151
152 let scale = self.config.dynamic_subdivision_scale;
156 let target_area = scale * scale;
157
158 ensure!(
159 target_area.is_normal(),
160 "nvidia omm squared subdivision scale must be finite and normal"
161 );
162
163 #[expect(
164 clippy::cast_precision_loss,
165 reason = "match the SDK's uint2 to float2 conversion"
166 )]
167 let size = texture_size.map(|value| value as f32);
168
169 for uv in self.texture_coordinates {
170 ensure!(
171 uv.iter().all(|value| value.is_finite()),
172 "nvidia omm texture coordinates must be finite"
173 );
174 }
175
176 for indices in self.indices.chunks_exact(3) {
177 let uv = [indices[0], indices[1], indices[2]]
178 .map(|index| self.texture_coordinates[index as usize]);
179 let p = uv.map(|uv| [uv[0] * size[0], uv[1] * size[1]]);
180 let a = [p[2][0] - p[0][0], p[2][1] - p[0][1]];
181 let b = [p[1][0] - p[0][0], p[1][1] - p[0][1]];
182 let cross = a[0] * b[1] - a[1] * b[0];
183 let pixel_area = 0.5 * (cross * cross).sqrt();
184 let ratio = pixel_area / target_area;
185
186 ensure!(
187 ratio.is_finite() && (0.0..4_294_967_296.0).contains(&ratio),
188 "nvidia omm dynamic subdivision area ratio is outside uint32 range"
189 );
190
191 for (start, end) in [(0, 1), (0, 2), (1, 2)] {
194 let edge = [
195 (uv[end][0] - uv[start][0]) * size[0],
196 (uv[end][1] - uv[start][1]) * size[1],
197 ];
198 let length_squared = edge[0] * edge[0] + edge[1] * edge[1];
199
200 ensure!(
201 length_squared.is_finite(),
202 "nvidia omm dynamic subdivision edge length must be finite"
203 );
204
205 }
208
209 ensure!(
213 p.iter()
214 .flatten()
215 .all(|value| value.abs() < 1_073_741_824.0),
216 "nvidia omm texture coordinates exceed the safe raster range"
217 );
218
219 let extent = [0, 1].map(|axis| {
225 let min_uv = uv[0][axis].min(uv[1][axis]).min(uv[2][axis]);
226 let max_uv = uv[0][axis].max(uv[1][axis]).max(uv[2][axis]);
227 let workload_extent = (max_uv - min_uv) * size[axis];
228 let min_p = p[0][axis].min(p[1][axis]).min(p[2][axis]);
229 let max_p = p[0][axis].max(p[1][axis]).max(p[2][axis]);
230 f64::from(workload_extent)
231 .max(f64::from(max_p) - f64::from(min_p))
232 .ceil()
233 + 4.0
234 });
235
236 ensure!(
237 extent[0] * extent[1] <= f64::from(i32::MAX),
238 "nvidia omm texel bounding box product exceeds the safe int32 range"
239 );
240 }
241
242 Ok(())
243 }
244}
245
246#[derive(Clone, Debug, PartialEq)]
247pub struct BakeOutput {
248 pub array_data: Box<[u8]>,
249 pub descriptors: Box<[OpacityMicromapDescriptor]>,
250 pub descriptor_usage: Box<[OpacityMicromapUsage]>,
251 pub indices: Box<[i32]>,
252 pub index_usage: Box<[OpacityMicromapUsage]>,
253 pub statistics: Statistics,
254}
255
256#[cfg(nvidia_omm_native)]
257impl BakeOutput {
258 unsafe fn copy<T: Copy>(pointer: *const T, count: u32) -> Result<Box<[T]>> {
259 if count == 0 {
260 return Ok(Box::new([]));
261 }
262
263 ensure!(
264 !pointer.is_null(),
265 "nvidia omm returned a null output pointer"
266 );
267
268 Ok(unsafe { std::slice::from_raw_parts(pointer, count as usize) }.into())
269 }
270
271 unsafe fn from_native(output: ffi::BakeResult) -> Result<Self> {
272 Ok(Self {
273 array_data: unsafe { Self::copy(output.array_data, output.array_data_size) }?,
274 descriptors: unsafe { Self::copy(output.descriptors, output.descriptor_count) }?,
275 descriptor_usage: unsafe {
276 Self::copy(output.descriptor_usage, output.descriptor_usage_count)
277 }?,
278 indices: unsafe { Self::copy(output.indices, output.index_count) }?,
279 index_usage: unsafe { Self::copy(output.index_usage, output.index_usage_count) }?,
280 statistics: output.statistics,
281 })
282 }
283}
284
285impl Baker {
286 #[must_use]
288 pub fn bundled_backend_path() -> Option<&'static std::path::Path> {
289 option_env!("NVIDIA_OMM_BACKEND").map(std::path::Path::new)
290 }
291
292 #[must_use]
294 pub const fn native_backend_available() -> bool {
295 cfg!(nvidia_omm_native)
296 }
297}
298
299#[derive(Clone, Copy, Debug, Eq, PartialEq)]
300#[repr(C)]
301pub struct OpacityMicromapDescriptor {
302 pub data_offset: u32,
303 pub subdivision_level: u16,
304 pub format: u16,
305}
306
307#[derive(Clone, Copy, Debug, Eq, PartialEq)]
308#[repr(C)]
309pub struct OpacityMicromapUsage {
310 pub count: u32,
311 pub subdivision_level: u16,
312 pub format: u16,
313}
314
315#[derive(Clone, Copy, Debug, Default, PartialEq)]
316#[repr(C)]
317pub struct Statistics {
318 pub opaque: u64,
319 pub transparent: u64,
320 pub unknown_transparent: u64,
321 pub unknown_opaque: u64,
322 pub fully_opaque: u32,
323 pub fully_transparent: u32,
324 pub fully_unknown_opaque: u32,
325 pub fully_unknown_transparent: u32,
326 pub known_area: f32,
327}
328
329#[cfg(test)]
330mod test {
331 use {
332 super::*,
333 std::mem::{align_of, offset_of, size_of},
334 };
335
336 #[test]
337 fn invalid_geometry_is_rejected() {
338 let texture_coordinates = [[0.0, 0.0]; 3];
339
340 assert!(
341 BakeInput {
342 texture_coordinates: &texture_coordinates,
343 indices: &[0, 1],
344 config: BakeConfig::default(),
345 }
346 .validate([2, 2])
347 .is_err()
348 );
349 }
350
351 #[test]
352 fn invalid_texture_is_rejected() {
353 assert!(
354 AlphaMip::validate_all(&[AlphaMip {
355 width: 2,
356 height: 2,
357 row_pitch: 1,
358 data: &[0; 4],
359 }])
360 .is_err()
361 );
362 }
363
364 #[test]
365 fn mip_data_only_requires_pixels_in_the_final_row() {
366 #[cfg(nvidia_omm_native)]
367 let baker = Baker::new(Baker::bundled_backend_path().unwrap()).unwrap();
368
369 for (width, height, row_pitch, required) in [
370 (2, 2, 4, 6),
371 (2, 1, u32::MAX, 2),
372 (2, 2, 0, 4),
373 (2, 2, 2, 4),
374 ] {
375 let data = vec![0; required];
376 let mip = AlphaMip {
377 width,
378 height,
379 row_pitch,
380 data: &data,
381 };
382 AlphaMip::validate_all(&[mip]).unwrap();
383
384 #[cfg(nvidia_omm_native)]
385 {
386 let texture = baker.create_texture(&[mip]).unwrap();
387 let output = baker
388 .bake(
389 &texture,
390 BakeInput {
391 texture_coordinates: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
392 indices: &[0, 1, 2],
393 config: BakeConfig::default(),
394 },
395 )
396 .unwrap();
397 assert_eq!(output.indices.as_ref(), &[-1]);
398 }
399
400 let truncated = AlphaMip {
401 data: &data[..required - 1],
402 ..mip
403 };
404 assert_eq!(
405 AlphaMip::validate_all(&[truncated])
406 .unwrap_err()
407 .to_string(),
408 "nvidia omm mip data is truncated"
409 );
410 #[cfg(nvidia_omm_native)]
411 assert!(baker.create_texture(&[truncated]).is_err());
412 }
413 }
414
415 #[test]
416 fn oversized_mip_data_is_rejected_without_wrapping() {
417 let error = AlphaMip::validate_all(&[AlphaMip {
418 width: u32::MAX,
419 height: u32::MAX,
420 row_pitch: u32::MAX,
421 data: &[],
422 }])
423 .unwrap_err();
424
425 if usize::BITS >= 64 {
426 assert_eq!(error.to_string(), "nvidia omm mip data is truncated");
427 } else {
428 assert_eq!(error.to_string(), "nvidia omm mip byte size overflow");
429 }
430 }
431
432 #[test]
433 fn mip_dimensions_must_be_non_increasing() {
434 #[cfg(nvidia_omm_native)]
435 let baker = Baker::new(Baker::bundled_backend_path().unwrap()).unwrap();
436
437 for dimensions in [
438 [[2, 2], [4, 2], [1, 1]],
439 [[2, 2], [2, 4], [1, 1]],
440 [[4, 4], [1, 2], [2, 1]],
441 [[4, 4], [2, 1], [1, 2]],
442 ] {
443 let mips = dimensions.map(|[width, height]| AlphaMip {
444 width,
445 height,
446 row_pitch: 0,
447 data: &[0; 16],
448 });
449 let error = AlphaMip::validate_all(&mips).unwrap_err().to_string();
450
451 assert_eq!(error, "nvidia omm mip dimensions must be non-increasing");
452
453 #[cfg(nvidia_omm_native)]
454 assert_eq!(
455 baker.create_texture(&mips).err().unwrap().to_string(),
456 error
457 );
458 }
459
460 for dimensions in [
461 [[4, 4], [2, 2], [1, 1]],
462 [[4, 4], [4, 4], [4, 4]],
463 [[4, 3], [3, 3], [3, 1]],
464 ] {
465 let mips = dimensions.map(|[width, height]| AlphaMip {
466 width,
467 height,
468 row_pitch: 0,
469 data: &[0; 16],
470 });
471 AlphaMip::validate_all(&mips).unwrap();
472 }
473 }
474
475 #[test]
476 fn unsafe_texel_bounding_box_products_are_rejected() {
477 #[cfg(nvidia_omm_native)]
478 let baker = Baker::new(Baker::bundled_backend_path().unwrap()).unwrap();
479
480 #[cfg(nvidia_omm_native)]
481 let texture = baker
482 .create_texture(&[AlphaMip {
483 width: 2,
484 height: 2,
485 row_pitch: 0,
486 data: &[0; 4],
487 }])
488 .unwrap();
489
490 for uv in [
491 [[0.0, 0.0], [32768.0, 32768.0], [16384.0, 16384.0]],
492 [[-16384.0, -16384.0], [16384.0, 16384.0], [0.0, 0.0]],
493 [[0.0, 0.0], [23169.0, 23169.0], [0.0, 0.0]],
495 [[0.0, 0.0], [23168.25, 23168.25], [0.0, 0.0]],
496 ] {
497 for validation in [false, true] {
498 let input = BakeInput {
499 texture_coordinates: &uv,
500 indices: &[0, 1, 2],
501 config: BakeConfig {
502 dynamic_subdivision_scale: 2.0,
503 max_subdivision_level: 0,
504 max_workload_size: 1,
505 validation,
506 ..BakeConfig::default()
507 },
508 };
509 let error = input.validate([2, 2]).unwrap_err().to_string();
510
511 assert_eq!(
512 error,
513 "nvidia omm texel bounding box product exceeds the safe int32 range"
514 );
515
516 #[cfg(nvidia_omm_native)]
517 assert_eq!(baker.bake(&texture, input).unwrap_err().to_string(), error);
518 }
519 }
520
521 for end in [[23168.0, 23168.0], [50000.0, 1.0]] {
522 BakeInput {
523 texture_coordinates: &[[0.0, 0.0], end, [0.0, 0.0]],
524 indices: &[0, 1, 2],
525 config: BakeConfig::default(),
526 }
527 .validate([2, 2])
528 .unwrap();
529 }
530 }
531
532 #[test]
533 fn unsafe_subdivision_arithmetic_is_rejected() {
534 let triangle = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
535
536 #[cfg(nvidia_omm_native)]
537 let baker = Baker::new(Baker::bundled_backend_path().unwrap()).unwrap();
538
539 #[cfg(nvidia_omm_native)]
540 let texture = baker
541 .create_texture(&[AlphaMip {
542 width: 2,
543 height: 2,
544 row_pitch: 0,
545 data: &[0; 4],
546 }])
547 .unwrap();
548
549 for (name, uv, scale) in [
550 ("squared scale underflow", triangle, 1e-30),
551 ("subnormal squared scale", triangle, 1e-20),
552 ("squared scale overflow", triangle, f32::MAX),
553 (
554 "finite ratio beyond uint32 limit",
555 triangle,
556 2.0_f32.powi(-16),
557 ),
558 (
559 "finite ratio at uint32 limit",
560 [triangle[0], triangle[1], [0.0, 0.5]],
561 2.0_f32.powi(-16),
562 ),
563 (
564 "ratio overflow",
565 [[0.0, 0.0], [2.0, 0.0], [0.0, 2.0]],
566 1.1e-19,
567 ),
568 ("zero scale", triangle, 0.0),
569 ("negative scale", triangle, -1.0),
570 ("nan scale", triangle, f32::NAN),
571 ("infinite scale", triangle, f32::INFINITY),
572 ("nan uv", [[f32::NAN, 0.0], triangle[1], triangle[2]], 2.0),
573 (
574 "infinite uv",
575 [[0.0, f32::INFINITY], triangle[1], triangle[2]],
576 2.0,
577 ),
578 ("negative infinite uv", [[f32::NEG_INFINITY, 0.0]; 3], 2.0),
579 ("texel overflow", [[f32::MAX, 0.0]; 3], 2.0),
580 (
581 "edge overflow",
582 [[-1e20, 0.0], [1e20, 0.0], [0.0, 0.0]],
583 2.0,
584 ),
585 (
586 "cross squared overflow",
587 [[0.0, 0.0], [1e10, 0.0], [0.0, 1e10]],
588 1e10,
589 ),
590 ("huge translated point", [[1e30, 1e30]; 3], 2.0),
591 ] {
592 for validation in [false, true] {
594 let input = BakeInput {
595 texture_coordinates: &uv,
596 indices: &[0, 1, 2],
597 config: BakeConfig {
598 dynamic_subdivision_scale: scale,
599 max_subdivision_level: 0,
600 validation,
601 ..BakeConfig::default()
602 },
603 };
604 let error = input.validate([2, 2]).expect_err(name).to_string();
605
606 assert!(error.starts_with("nvidia omm"), "{name}: {error}");
607
608 #[cfg(nvidia_omm_native)]
609 assert_eq!(
610 baker.bake(&texture, input).unwrap_err().to_string(),
611 error,
612 "{name}"
613 );
614 }
615 }
616 }
617
618 #[test]
619 fn ordinary_subdivision_arithmetic_is_accepted() {
620 for uv in [
621 [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
622 [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]],
623 [[-1.0, -1.0], [2.0, -1.0], [-1.0, 2.0]],
624 [[0.0, 0.0]; 3],
625 [[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]],
626 ] {
627 for scale in [2.0, 1e19] {
628 BakeInput {
629 texture_coordinates: &uv,
630 indices: &[0, 1, 2],
631 config: BakeConfig {
632 dynamic_subdivision_scale: scale,
633 ..BakeConfig::default()
634 },
635 }
636 .validate([2, 2])
637 .unwrap();
638 }
639 }
640
641 let input = BakeInput {
642 texture_coordinates: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
643 indices: &[0, 1, 2],
644 config: BakeConfig {
645 dynamic_subdivision_scale: 2.0_f32.powi(-15),
646 ..BakeConfig::default()
647 },
648 };
649
650 assert!(input.validate([2, 2]).is_ok());
651 assert!(input.validate([4, 4]).is_err());
652 }
653
654 #[cfg(nvidia_omm_native)]
655 #[test]
656 fn native_baker_returns_owned_special_indices() {
657 let baker = Baker::new(Baker::bundled_backend_path().unwrap()).unwrap();
658 let texture = baker
659 .create_texture(&[AlphaMip {
660 width: 2,
661 height: 2,
662 row_pitch: 0,
663 data: &[0; 4],
664 }])
665 .unwrap();
666 let output = baker
667 .bake(
668 &texture,
669 BakeInput {
670 texture_coordinates: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
671 indices: &[0, 1, 2],
672 config: BakeConfig::default(),
673 },
674 )
675 .unwrap();
676
677 assert_eq!(output.indices.as_ref(), &[-1]);
678 assert_eq!(output.statistics.fully_transparent, 1);
679 }
680
681 #[test]
682 fn native_ffi_layouts_are_stable() {
683 assert_eq!(align_of::<OpacityMicromapDescriptor>(), 4);
684 assert_eq!(offset_of!(OpacityMicromapDescriptor, subdivision_level), 4);
685 assert_eq!(size_of::<OpacityMicromapDescriptor>(), 8);
686 assert_eq!(size_of::<OpacityMicromapUsage>(), 8);
687 assert_eq!(size_of::<Statistics>(), 56);
688 assert_eq!(size_of::<ffi::TextureMip>(), 24);
689 assert_eq!(size_of::<ffi::BakeInput>(), 64);
690 assert_eq!(size_of::<ffi::BakeResult>(), 136);
691 }
692}