1use std::collections::VecDeque;
2
3use cubecl::Runtime;
4
5use crate::accelerate::Accelerator;
6use crate::device::Device;
7use crate::nlmeans::{ChannelMode, MotionCompensationMode, NlmDenoiser, NlmParams, Pending, PrefilterMode};
8use crate::sniff::sniff_best_accelerator;
9
10#[derive(Debug, Clone, bon::Builder)]
12pub struct DenoiserOptions {
13 #[builder(default = ChannelMode::Yuv)]
15 pub channel_mode: ChannelMode,
16 #[builder(default = DenoisingMode::Spacial)]
18 pub mode: DenoisingMode,
19 #[builder(default = PrefilterMode::None)]
21 pub prefilter: PrefilterMode,
22 #[builder(default = MotionCompensationMode::None)]
27 pub motion_compensation: MotionCompensationMode,
28 pub nlm: Option<NlmTuning>,
31}
32
33#[derive(Debug, Copy, Clone, Eq, PartialEq)]
35pub enum DenoisingMode {
36 Spacial,
38 Temporal { radius: u32 },
40}
41
42#[derive(Debug, Copy, Clone)]
45pub struct NlmTuning {
46 pub search_radius: Option<u32>,
47 pub patch_radius: Option<u32>,
48 pub strength: Option<f32>,
49 pub self_weight: Option<f32>,
50}
51
52impl DenoiserOptions {
53 fn to_nlm_params(&self) -> NlmParams {
54 let mut params = NlmParams {
55 channels: self.channel_mode,
56 prefilter: self.prefilter,
57 motion_compensation: self.motion_compensation,
58 temporal_radius: match self.mode {
59 DenoisingMode::Spacial => 0,
60 DenoisingMode::Temporal { radius } => radius,
61 },
62 ..NlmParams::default()
63 };
64 if let Some(t) = self.nlm {
65 if let Some(v) = t.search_radius {
66 params.search_radius = v;
67 }
68 if let Some(v) = t.patch_radius {
69 params.patch_radius = v;
70 }
71 if let Some(v) = t.strength {
72 params.strength = v;
73 }
74 if let Some(v) = t.self_weight {
75 params.self_weight = v;
76 }
77 }
78 params
79 }
80}
81
82#[derive(Debug, thiserror::Error)]
84pub enum DenoiserError {
85 #[error("denoiser queue is full; collect the pending frame before pushing more")]
90 QueueFull,
91 #[error("no accelerator from the priority list is available")]
93 NoAcceleratorAvailable,
94 #[error(transparent)]
97 Other(#[from] anyhow::Error),
98}
99
100enum Backend {
101 #[cfg(feature = "cuda")]
102 Cuda(NlmDenoiser<cubecl::cuda::CudaRuntime>),
103 #[cfg(feature = "rocm")]
104 Rocm(NlmDenoiser<cubecl::hip::HipRuntime>),
105 #[cfg(any(feature = "vulkan", feature = "metal"))]
106 Wgpu(NlmDenoiser<cubecl::wgpu::WgpuRuntime>),
107 #[cfg(feature = "cpu")]
108 Cpu(NlmDenoiser<cubecl::cpu::CpuRuntime>),
109}
110
111enum BackendPending {
112 #[cfg(feature = "cuda")]
113 Cuda(Pending<cubecl::cuda::CudaRuntime>),
114 #[cfg(feature = "rocm")]
115 Rocm(Pending<cubecl::hip::HipRuntime>),
116 #[cfg(any(feature = "vulkan", feature = "metal"))]
117 Wgpu(Pending<cubecl::wgpu::WgpuRuntime>),
118 #[cfg(feature = "cpu")]
119 Cpu(Pending<cubecl::cpu::CpuRuntime>),
120}
121
122impl BackendPending {
123 fn wait(self) -> Result<Vec<f32>, anyhow::Error> {
124 match self {
125 #[cfg(feature = "cuda")]
126 Self::Cuda(p) => p.wait(),
127 #[cfg(feature = "rocm")]
128 Self::Rocm(p) => p.wait(),
129 #[cfg(any(feature = "vulkan", feature = "metal"))]
130 Self::Wgpu(p) => p.wait(),
131 #[cfg(feature = "cpu")]
132 Self::Cpu(p) => p.wait(),
133 }
134 }
135}
136
137const MAX_PENDING: usize = 2;
148
149pub struct Denoiser {
150 backend: Backend,
151 pending: VecDeque<BackendPending>,
152 accelerator: Accelerator,
153 width: u32,
154 height: u32,
155 channels: u32,
156 temporal_radius: u32,
157 frames_pushed: u32,
158}
159
160impl Denoiser {
161 pub fn create(
185 accelerators: &[Accelerator],
186 device: &Device,
187 width: u32,
188 height: u32,
189 options: DenoiserOptions,
190 ) -> Result<Self, DenoiserError> {
191 let accelerator =
192 sniff_best_accelerator(accelerators).ok_or(DenoiserError::NoAcceleratorAvailable)?;
193
194 let params = options.to_nlm_params();
195 params.validate()?;
196
197 let channels = params.channels.count();
198 let temporal_radius = params.temporal_radius;
199 let backend = build_backend(accelerator, device, params, width, height)?;
200
201 Ok(Self {
202 backend,
203 pending: VecDeque::with_capacity(MAX_PENDING),
204 accelerator,
205 width,
206 height,
207 channels,
208 temporal_radius,
209 frames_pushed: 0,
210 })
211 }
212
213 pub fn selected_accelerator(&self) -> Accelerator {
215 self.accelerator
216 }
217
218 pub fn width(&self) -> u32 {
220 self.width
221 }
222
223 pub fn height(&self) -> u32 {
225 self.height
226 }
227
228 pub fn push_frame(&mut self, frame: &[f32]) -> Result<(), DenoiserError> {
239 let window_full = self.frames_pushed > self.temporal_radius;
243 if window_full && self.pending.len() >= MAX_PENDING {
244 return Err(DenoiserError::QueueFull);
245 }
246
247 match &mut self.backend {
248 #[cfg(feature = "cuda")]
249 Backend::Cuda(d) => {
250 d.push_frame(frame);
251 if let Some(p) = d.denoise_submit()? {
252 self.pending.push_back(BackendPending::Cuda(p));
253 }
254 },
255 #[cfg(feature = "rocm")]
256 Backend::Rocm(d) => {
257 d.push_frame(frame);
258 if let Some(p) = d.denoise_submit()? {
259 self.pending.push_back(BackendPending::Rocm(p));
260 }
261 },
262 #[cfg(any(feature = "vulkan", feature = "metal"))]
263 Backend::Wgpu(d) => {
264 d.push_frame(frame);
265 if let Some(p) = d.denoise_submit()? {
266 self.pending.push_back(BackendPending::Wgpu(p));
267 }
268 },
269 #[cfg(feature = "cpu")]
270 Backend::Cpu(d) => {
271 d.push_frame(frame);
272 if let Some(p) = d.denoise_submit()? {
273 self.pending.push_back(BackendPending::Cpu(p));
274 }
275 },
276 }
277
278 self.frames_pushed = self.frames_pushed.saturating_add(1);
279 Ok(())
280 }
281
282 pub fn recv_frame(&mut self) -> Result<Option<Vec<f32>>, DenoiserError> {
286 let Some(pending) = self.pending.pop_front() else {
287 return Ok(None);
288 };
289 Ok(Some(pending.wait()?))
290 }
291
292 pub fn try_recv_frame(&mut self) -> Result<Option<Vec<f32>>, DenoiserError> {
297 self.recv_frame()
298 }
299
300 pub fn flush(&mut self, mut sink: impl FnMut(Vec<f32>)) -> Result<(), DenoiserError> {
311 while let Some(frame) = self.recv_frame()? {
314 sink(frame);
315 }
316
317 let pixels = (self.width * self.height) as usize;
318 let channels = self.channels as usize;
319 let scratch_cap = pixels * channels;
320
321 match &mut self.backend {
322 #[cfg(feature = "cuda")]
323 Backend::Cuda(d) => d.flush(|slice| {
324 let mut v = Vec::with_capacity(scratch_cap);
325 v.extend_from_slice(slice);
326 sink(v);
327 })?,
328 #[cfg(feature = "rocm")]
329 Backend::Rocm(d) => d.flush(|slice| {
330 let mut v = Vec::with_capacity(scratch_cap);
331 v.extend_from_slice(slice);
332 sink(v);
333 })?,
334 #[cfg(any(feature = "vulkan", feature = "metal"))]
335 Backend::Wgpu(d) => d.flush(|slice| {
336 let mut v = Vec::with_capacity(scratch_cap);
337 v.extend_from_slice(slice);
338 sink(v);
339 })?,
340 #[cfg(feature = "cpu")]
341 Backend::Cpu(d) => d.flush(|slice| {
342 let mut v = Vec::with_capacity(scratch_cap);
343 v.extend_from_slice(slice);
344 sink(v);
345 })?,
346 }
347
348 self.frames_pushed = 0;
352
353 Ok(())
354 }
355}
356
357fn build_backend(
358 accel: Accelerator,
359 device: &Device,
360 params: NlmParams,
361 width: u32,
362 height: u32,
363) -> Result<Backend, DenoiserError> {
364 match accel {
365 #[cfg(feature = "cuda")]
366 Accelerator::Cuda => {
367 let dev = device.to_cuda()?;
368 let client = <cubecl::cuda::CudaRuntime as Runtime>::client(&dev);
369 Ok(Backend::Cuda(NlmDenoiser::new(&client, params, width, height)))
370 },
371 #[cfg(feature = "rocm")]
372 Accelerator::Rocm => {
373 let dev = device.to_amd()?;
374 let client = <cubecl::hip::HipRuntime as Runtime>::client(&dev);
375 Ok(Backend::Rocm(NlmDenoiser::new(&client, params, width, height)))
376 },
377 #[cfg(feature = "vulkan")]
378 Accelerator::Vulkan => {
379 let dev = device.to_wgpu()?;
380 let client = <cubecl::wgpu::WgpuRuntime as Runtime>::client(&dev);
381 Ok(Backend::Wgpu(NlmDenoiser::new(&client, params, width, height)))
382 },
383 #[cfg(feature = "metal")]
384 Accelerator::Metal => {
385 let dev = device.to_wgpu()?;
386 let client = <cubecl::wgpu::WgpuRuntime as Runtime>::client(&dev);
387 Ok(Backend::Wgpu(NlmDenoiser::new(&client, params, width, height)))
388 },
389 #[cfg(feature = "cpu")]
390 Accelerator::Cpu => {
391 let dev = device.to_cpu()?;
392 let client = <cubecl::cpu::CpuRuntime as Runtime>::client(&dev);
393 Ok(Backend::Cpu(NlmDenoiser::new(&client, params, width, height)))
394 },
395 #[cfg(docsrs)]
399 #[allow(unreachable_patterns)]
400 _ => unreachable!(),
401 }
402}
403
404#[cfg(test)]
405mod options_tests {
406 use super::*;
407
408 #[test]
409 fn spatial_mode_maps_to_zero_temporal_radius() {
410 let opts = DenoiserOptions::builder()
411 .channel_mode(ChannelMode::Yuv)
412 .mode(DenoisingMode::Spacial)
413 .build();
414 let params = opts.to_nlm_params();
415
416 assert_eq!(params.temporal_radius, 0);
417 assert_eq!(params.channels, ChannelMode::Yuv);
418 }
419
420 #[test]
421 fn temporal_mode_propagates_radius() {
422 let opts = DenoiserOptions::builder()
423 .mode(DenoisingMode::Temporal { radius: 3 })
424 .build();
425 let params = opts.to_nlm_params();
426
427 assert_eq!(params.temporal_radius, 3);
428 }
429
430 #[test]
431 fn prefilter_passthrough() {
432 let opts = DenoiserOptions::builder()
433 .prefilter(PrefilterMode::Bilateral {
434 sigma_s: 3.0,
435 sigma_r: 0.02,
436 })
437 .build();
438 let params = opts.to_nlm_params();
439
440 assert!(matches!(params.prefilter, PrefilterMode::Bilateral { .. }));
441 }
442
443 #[test]
444 fn motion_compensation_passthrough() {
445 let opts = DenoiserOptions::builder()
446 .mode(DenoisingMode::Temporal { radius: 1 })
447 .motion_compensation(MotionCompensationMode::Mvtools {
448 blksize: 16,
449 overlap: 8,
450 search_radius: 4,
451 pyramid_levels: 2,
452 })
453 .build();
454 let params = opts.to_nlm_params();
455
456 assert!(matches!(
457 params.motion_compensation,
458 MotionCompensationMode::Mvtools {
459 blksize: 16,
460 overlap: 8,
461 search_radius: 4,
462 pyramid_levels: 2,
463 }
464 ));
465 }
466
467 #[test]
468 fn motion_compensation_defaults_to_none() {
469 let opts = DenoiserOptions::builder().build();
470 let params = opts.to_nlm_params();
471 assert!(matches!(params.motion_compensation, MotionCompensationMode::None));
472 }
473
474 #[test]
475 fn nlm_tuning_overrides_individual_fields() {
476 let defaults = NlmParams::default();
477 let opts = DenoiserOptions::builder()
478 .nlm(NlmTuning {
479 search_radius: Some(7),
480 patch_radius: None,
481 strength: Some(2.5),
482 self_weight: None,
483 })
484 .build();
485 let params = opts.to_nlm_params();
486
487 assert_eq!(params.search_radius, 7);
488 assert_eq!(params.patch_radius, defaults.patch_radius);
489 assert!((params.strength - 2.5).abs() < f32::EPSILON);
490 assert!((params.self_weight - defaults.self_weight).abs() < f32::EPSILON);
491 }
492}
493
494#[cfg(all(test, feature = "cpu"))]
495mod tests {
496 use super::*;
497
498 fn opts(mode: DenoisingMode) -> DenoiserOptions {
499 DenoiserOptions::builder()
500 .channel_mode(ChannelMode::Luma)
501 .mode(mode)
502 .build()
503 }
504
505 fn frame(w: u32, h: u32) -> Vec<f32> {
506 vec![0.5f32; (w * h) as usize]
507 }
508
509 #[test]
510 fn spatial_denoise_roundtrip() {
511 let mut d = Denoiser::create(
512 &[Accelerator::Cpu],
513 &Device::Default,
514 16,
515 16,
516 opts(DenoisingMode::Spacial),
517 )
518 .expect("denoiser construction failed");
519 assert_eq!(d.selected_accelerator(), Accelerator::Cpu);
520
521 d.push_frame(&frame(16, 16)).expect("push failed");
522 let out = d.recv_frame().expect("recv failed").expect("no frame");
523 assert_eq!(out.len(), 16 * 16);
524 }
525
526 #[test]
527 fn invalid_params_surface_as_error() {
528 let bad = DenoiserOptions::builder()
529 .nlm(NlmTuning {
530 search_radius: None,
531 patch_radius: None,
532 strength: Some(0.0),
533 self_weight: None,
534 })
535 .build();
536 let result = Denoiser::create(&[Accelerator::Cpu], &Device::Default, 16, 16, bad);
537
538 match result {
539 Err(DenoiserError::Other(_)) => {},
540 Err(other) => panic!("expected DenoiserError::Other, got {other:?}"),
541 Ok(_) => panic!("expected validation error, got Ok"),
542 }
543 }
544
545 #[test]
546 fn push_after_pending_returns_queue_full() {
547 let mut d = Denoiser::create(
548 &[Accelerator::Cpu],
549 &Device::Default,
550 16,
551 16,
552 opts(DenoisingMode::Spacial),
553 )
554 .unwrap();
555
556 d.push_frame(&frame(16, 16)).unwrap();
560 d.push_frame(&frame(16, 16)).unwrap();
561 let err = d.push_frame(&frame(16, 16)).expect_err("expected QueueFull");
562 assert!(matches!(err, DenoiserError::QueueFull));
563
564 let out = d.recv_frame().unwrap().unwrap();
565 assert_eq!(out.len(), 16 * 16);
566
567 d.push_frame(&frame(16, 16)).expect("push after drain failed");
569 }
570
571 fn frame_filled(w: u32, h: u32, value: f32) -> Vec<f32> {
572 vec![value; (w * h) as usize]
573 }
574
575 fn push_n_with_drain(d: &mut Denoiser, n: usize, value: f32, out: &mut Vec<Vec<f32>>) {
578 for _ in 0..n {
579 loop {
580 match d.push_frame(&frame_filled(16, 16, value)) {
581 Ok(()) => break,
582 Err(DenoiserError::QueueFull) => {
583 let f = d
584 .recv_frame()
585 .expect("recv ok")
586 .expect("queue full but recv yielded none");
587 out.push(f);
588 },
589 Err(e) => panic!("unexpected push error: {e:?}"),
590 }
591 }
592 }
593 }
594
595 #[test]
596 fn flush_leaves_denoiser_reusable_spatial() {
597 let mut d = Denoiser::create(
598 &[Accelerator::Cpu],
599 &Device::Default,
600 16,
601 16,
602 opts(DenoisingMode::Spacial),
603 )
604 .unwrap();
605
606 let mut batch_a = Vec::new();
607 push_n_with_drain(&mut d, 5, 0.25, &mut batch_a);
608 d.flush(|f| batch_a.push(f)).expect("first flush failed");
609 assert_eq!(batch_a.len(), 5);
610
611 assert!(d.recv_frame().unwrap().is_none());
613
614 let mut batch_b = Vec::new();
615 push_n_with_drain(&mut d, 5, 0.75, &mut batch_b);
616 d.flush(|f| batch_b.push(f)).expect("second flush failed");
617 assert_eq!(batch_b.len(), 5);
618
619 for v in batch_b.iter().flatten() {
620 assert!((v - 0.75).abs() < 0.1, "batch_b carried state from batch_a: {v}");
621 }
622 for v in batch_a.iter().flatten() {
623 assert!((v - 0.25).abs() < 0.1, "batch_a value unexpectedly drifted: {v}");
624 }
625 }
626
627 #[test]
628 fn flush_leaves_denoiser_reusable_temporal() {
629 let mut d = Denoiser::create(
630 &[Accelerator::Cpu],
631 &Device::Default,
632 16,
633 16,
634 opts(DenoisingMode::Temporal { radius: 1 }),
635 )
636 .unwrap();
637
638 let mut batch_a = Vec::new();
639 push_n_with_drain(&mut d, 5, 0.25, &mut batch_a);
640 d.flush(|f| batch_a.push(f)).expect("first flush failed");
641 assert_eq!(batch_a.len(), 5, "expected 5 frames from first batch");
642
643 assert!(d.recv_frame().unwrap().is_none());
647 d.push_frame(&frame_filled(16, 16, 0.75)).unwrap();
648 assert!(
649 d.recv_frame().unwrap().is_none(),
650 "first push of new temporal stream should not produce output yet"
651 );
652
653 let mut batch_b = Vec::new();
655 push_n_with_drain(&mut d, 4, 0.75, &mut batch_b);
656 d.flush(|f| batch_b.push(f)).expect("second flush failed");
657 assert_eq!(batch_b.len(), 5, "expected 5 frames from second batch");
658
659 for v in batch_b.iter().flatten() {
660 assert!((v - 0.75).abs() < 0.1, "batch_b carried state from batch_a: {v}");
661 }
662 }
663
664 #[test]
665 fn flush_emits_exactly_n_outputs_for_small_n() {
666 for n in 1..=5usize {
671 let mut d = Denoiser::create(
672 &[Accelerator::Cpu],
673 &Device::Default,
674 16,
675 16,
676 opts(DenoisingMode::Temporal { radius: 2 }),
677 )
678 .unwrap();
679
680 let mut out = Vec::new();
681 push_n_with_drain(&mut d, n, 0.5, &mut out);
682 d.flush(|f| out.push(f)).expect("flush failed");
683 assert_eq!(
684 out.len(),
685 n,
686 "expected {n} outputs for {n} pushes, got {}",
687 out.len()
688 );
689 }
690 }
691}