1use std::collections::VecDeque;
2
3use cubecl::Runtime;
4
5use crate::accelerate::Accelerator;
6use crate::device::Device;
7#[cfg(test)]
8use crate::nlmeans::MotionEstimation;
9use crate::nlmeans::{
10 ChannelMode,
11 HqParams,
12 MotionCompensationMode,
13 NlmDenoiser,
14 NlmParams,
15 Pending,
16 PrefilterMode,
17 hq_default_strength,
18 validate_dimensions,
19};
20use crate::sniff::sniff_best_accelerator;
21
22#[derive(Debug, Clone, bon::Builder)]
24pub struct DenoiserOptions {
25 #[builder(default = ChannelMode::Yuv)]
27 pub channel_mode: ChannelMode,
28 #[builder(default = DenoisingMode::Spacial)]
30 pub mode: DenoisingMode,
31 #[builder(default = Algorithm::Nlmeans)]
38 pub algorithm: Algorithm,
39 pub prefilter: Option<PrefilterMode>,
44 #[builder(default = MotionCompensationMode::None)]
49 pub motion_compensation: MotionCompensationMode,
50 pub nlm: Option<NlmTuning>,
53}
54
55#[derive(Debug, Copy, Clone, PartialEq)]
57pub enum Algorithm {
58 Nlmeans,
60 NlmeansHq(HqParams),
62}
63
64#[derive(Debug, Copy, Clone, Eq, PartialEq)]
66pub enum DenoisingMode {
67 Spacial,
69 Temporal { radius: u32 },
71}
72
73#[derive(Debug, Copy, Clone)]
76pub struct NlmTuning {
77 pub search_radius: Option<u32>,
78 pub patch_radius: Option<u32>,
79 pub strength: Option<f32>,
80 pub self_weight: Option<f32>,
81}
82
83impl DenoiserOptions {
84 #[doc(hidden)]
91 pub fn to_nlm_params(&self) -> NlmParams {
92 let temporal_radius = match self.mode {
93 DenoisingMode::Spacial => 0,
94 DenoisingMode::Temporal { radius } => radius,
95 };
96
97 let explicit_strength = self.nlm.and_then(|t| t.strength);
110 let strength = explicit_strength.unwrap_or(match self.algorithm {
111 Algorithm::NlmeansHq(hq) if hq.auto_strength => {
118 hq_default_strength(self.channel_mode, temporal_radius)
119 },
120 Algorithm::NlmeansHq(_) | Algorithm::Nlmeans => NlmParams::default().strength,
121 });
122
123 let mut params = NlmParams {
124 channels: self.channel_mode,
125 prefilter: self.prefilter.unwrap_or(PrefilterMode::None),
126 motion_compensation: self.motion_compensation,
127 temporal_radius,
128 hq: match self.algorithm {
129 Algorithm::Nlmeans => None,
130 Algorithm::NlmeansHq(hq) => Some(hq),
131 },
132 strength,
133 ..NlmParams::default()
134 };
135 if let Some(t) = self.nlm {
136 if let Some(v) = t.search_radius {
137 params.search_radius = v;
138 }
139 if let Some(v) = t.patch_radius {
140 params.patch_radius = v;
141 }
142 if let Some(v) = t.self_weight {
143 params.self_weight = v;
144 }
145 }
146 params
147 }
148}
149
150#[derive(Debug, thiserror::Error)]
152pub enum DenoiserError {
153 #[error("denoiser queue is full; collect the pending frame before pushing more")]
158 QueueFull,
159 #[error("no accelerator from the priority list is available")]
161 NoAcceleratorAvailable,
162 #[error(transparent)]
165 Other(#[from] anyhow::Error),
166}
167
168enum Backend {
169 #[cfg(feature = "cuda")]
170 Cuda(NlmDenoiser<cubecl::cuda::CudaRuntime>),
171 #[cfg(feature = "rocm")]
172 Rocm(NlmDenoiser<cubecl::hip::HipRuntime>),
173 #[cfg(any(feature = "vulkan", feature = "metal"))]
174 Wgpu(NlmDenoiser<cubecl::wgpu::WgpuRuntime>),
175 #[cfg(feature = "cpu")]
176 Cpu(NlmDenoiser<cubecl::cpu::CpuRuntime>),
177}
178
179enum BackendPending {
180 #[cfg(feature = "cuda")]
181 Cuda(Pending<cubecl::cuda::CudaRuntime>),
182 #[cfg(feature = "rocm")]
183 Rocm(Pending<cubecl::hip::HipRuntime>),
184 #[cfg(any(feature = "vulkan", feature = "metal"))]
185 Wgpu(Pending<cubecl::wgpu::WgpuRuntime>),
186 #[cfg(feature = "cpu")]
187 Cpu(Pending<cubecl::cpu::CpuRuntime>),
188}
189
190impl BackendPending {
191 fn wait(self) -> Result<Vec<f32>, anyhow::Error> {
192 match self {
193 #[cfg(feature = "cuda")]
194 Self::Cuda(p) => p.wait(),
195 #[cfg(feature = "rocm")]
196 Self::Rocm(p) => p.wait(),
197 #[cfg(any(feature = "vulkan", feature = "metal"))]
198 Self::Wgpu(p) => p.wait(),
199 #[cfg(feature = "cpu")]
200 Self::Cpu(p) => p.wait(),
201 }
202 }
203}
204
205const MAX_PENDING: usize = 2;
216
217pub struct Denoiser {
218 backend: Backend,
219 pending: VecDeque<BackendPending>,
220 accelerator: Accelerator,
221 width: u32,
222 height: u32,
223 channels: u32,
224 temporal_radius: u32,
225 frames_pushed: u32,
226}
227
228impl Denoiser {
229 pub fn create(
253 accelerators: &[Accelerator],
254 device: &Device,
255 width: u32,
256 height: u32,
257 options: DenoiserOptions,
258 ) -> Result<Self, DenoiserError> {
259 let accelerator =
260 sniff_best_accelerator(accelerators).ok_or(DenoiserError::NoAcceleratorAvailable)?;
261
262 let params = options.to_nlm_params();
263 params.validate()?;
264 validate_dimensions(width, height)?;
265
266 let channels = params.channels.count();
267 let temporal_radius = params.temporal_radius;
268 let backend = build_backend(accelerator, device, params, width, height)?;
269
270 Ok(Self {
271 backend,
272 pending: VecDeque::with_capacity(MAX_PENDING),
273 accelerator,
274 width,
275 height,
276 channels,
277 temporal_radius,
278 frames_pushed: 0,
279 })
280 }
281
282 pub fn selected_accelerator(&self) -> Accelerator {
284 self.accelerator
285 }
286
287 pub fn width(&self) -> u32 {
289 self.width
290 }
291
292 pub fn height(&self) -> u32 {
294 self.height
295 }
296
297 pub fn push_frame(&mut self, frame: &[f32]) -> Result<(), DenoiserError> {
308 let window_full = self.frames_pushed > self.temporal_radius;
312 if window_full && self.pending.len() >= MAX_PENDING {
313 return Err(DenoiserError::QueueFull);
314 }
315
316 match &mut self.backend {
317 #[cfg(feature = "cuda")]
318 Backend::Cuda(d) => {
319 d.push_frame(frame);
320 if let Some(p) = d.denoise_submit()? {
321 self.pending.push_back(BackendPending::Cuda(p));
322 }
323 },
324 #[cfg(feature = "rocm")]
325 Backend::Rocm(d) => {
326 d.push_frame(frame);
327 if let Some(p) = d.denoise_submit()? {
328 self.pending.push_back(BackendPending::Rocm(p));
329 }
330 },
331 #[cfg(any(feature = "vulkan", feature = "metal"))]
332 Backend::Wgpu(d) => {
333 d.push_frame(frame);
334 if let Some(p) = d.denoise_submit()? {
335 self.pending.push_back(BackendPending::Wgpu(p));
336 }
337 },
338 #[cfg(feature = "cpu")]
339 Backend::Cpu(d) => {
340 d.push_frame(frame);
341 if let Some(p) = d.denoise_submit()? {
342 self.pending.push_back(BackendPending::Cpu(p));
343 }
344 },
345 }
346
347 self.frames_pushed = self.frames_pushed.saturating_add(1);
348 Ok(())
349 }
350
351 pub fn recv_frame(&mut self) -> Result<Option<Vec<f32>>, DenoiserError> {
355 let Some(pending) = self.pending.pop_front() else {
356 return Ok(None);
357 };
358 Ok(Some(pending.wait()?))
359 }
360
361 pub fn try_recv_frame(&mut self) -> Result<Option<Vec<f32>>, DenoiserError> {
366 self.recv_frame()
367 }
368
369 pub fn flush(&mut self, mut sink: impl FnMut(Vec<f32>)) -> Result<(), DenoiserError> {
380 while let Some(frame) = self.recv_frame()? {
383 sink(frame);
384 }
385
386 let pixels = (self.width * self.height) as usize;
387 let channels = self.channels as usize;
388 let scratch_cap = pixels * channels;
389
390 match &mut self.backend {
391 #[cfg(feature = "cuda")]
392 Backend::Cuda(d) => d.flush(|slice| {
393 let mut v = Vec::with_capacity(scratch_cap);
394 v.extend_from_slice(slice);
395 sink(v);
396 })?,
397 #[cfg(feature = "rocm")]
398 Backend::Rocm(d) => d.flush(|slice| {
399 let mut v = Vec::with_capacity(scratch_cap);
400 v.extend_from_slice(slice);
401 sink(v);
402 })?,
403 #[cfg(any(feature = "vulkan", feature = "metal"))]
404 Backend::Wgpu(d) => d.flush(|slice| {
405 let mut v = Vec::with_capacity(scratch_cap);
406 v.extend_from_slice(slice);
407 sink(v);
408 })?,
409 #[cfg(feature = "cpu")]
410 Backend::Cpu(d) => d.flush(|slice| {
411 let mut v = Vec::with_capacity(scratch_cap);
412 v.extend_from_slice(slice);
413 sink(v);
414 })?,
415 }
416
417 self.frames_pushed = 0;
421
422 Ok(())
423 }
424}
425
426fn build_backend(
427 accel: Accelerator,
428 device: &Device,
429 params: NlmParams,
430 width: u32,
431 height: u32,
432) -> Result<Backend, DenoiserError> {
433 match accel {
434 #[cfg(feature = "cuda")]
435 Accelerator::Cuda => {
436 let dev = device.to_cuda()?;
437 let client = <cubecl::cuda::CudaRuntime as Runtime>::client(&dev);
438 Ok(Backend::Cuda(NlmDenoiser::new(&client, params, width, height)))
439 },
440 #[cfg(feature = "rocm")]
441 Accelerator::Rocm => {
442 let dev = device.to_amd()?;
443 let client = <cubecl::hip::HipRuntime as Runtime>::client(&dev);
444 Ok(Backend::Rocm(NlmDenoiser::new(&client, params, width, height)))
445 },
446 #[cfg(feature = "vulkan")]
447 Accelerator::Vulkan => {
448 let dev = device.to_wgpu()?;
449 let client = <cubecl::wgpu::WgpuRuntime as Runtime>::client(&dev);
450 Ok(Backend::Wgpu(NlmDenoiser::new(&client, params, width, height)))
451 },
452 #[cfg(feature = "metal")]
453 Accelerator::Metal => {
454 let dev = device.to_wgpu()?;
455 let client = <cubecl::wgpu::WgpuRuntime as Runtime>::client(&dev);
456 Ok(Backend::Wgpu(NlmDenoiser::new(&client, params, width, height)))
457 },
458 #[cfg(feature = "cpu")]
459 Accelerator::Cpu => {
460 let dev = device.to_cpu()?;
461 let client = <cubecl::cpu::CpuRuntime as Runtime>::client(&dev);
462 Ok(Backend::Cpu(NlmDenoiser::new(&client, params, width, height)))
463 },
464 #[cfg(docsrs)]
468 #[allow(unreachable_patterns)]
469 _ => unreachable!(),
470 }
471}
472
473#[cfg(test)]
474mod options_tests {
475 use super::*;
476
477 #[test]
478 fn spatial_mode_maps_to_zero_temporal_radius() {
479 let opts = DenoiserOptions::builder()
480 .channel_mode(ChannelMode::Yuv)
481 .mode(DenoisingMode::Spacial)
482 .build();
483 let params = opts.to_nlm_params();
484
485 assert_eq!(params.temporal_radius, 0);
486 assert_eq!(params.channels, ChannelMode::Yuv);
487 }
488
489 #[test]
490 fn temporal_mode_propagates_radius() {
491 let opts = DenoiserOptions::builder()
492 .mode(DenoisingMode::Temporal { radius: 3 })
493 .build();
494 let params = opts.to_nlm_params();
495
496 assert_eq!(params.temporal_radius, 3);
497 }
498
499 #[test]
500 fn prefilter_passthrough() {
501 let opts = DenoiserOptions::builder()
502 .prefilter(PrefilterMode::Bilateral {
503 sigma_s: 3.0,
504 sigma_r: 0.02,
505 })
506 .build();
507 let params = opts.to_nlm_params();
508
509 assert!(matches!(params.prefilter, PrefilterMode::Bilateral { .. }));
510 }
511
512 #[test]
513 fn hq_unset_prefilter_defaults_to_none() {
514 let opts = DenoiserOptions::builder()
515 .algorithm(Algorithm::NlmeansHq(HqParams {
516 auto_strength: true,
517 noise_floor: true,
518 sigma_override: None,
519 temporal_confidence: true,
520 thsad_scale: 1.0,
521 sigma_scale: 1.0,
522 }))
523 .build();
524 let params = opts.to_nlm_params();
525
526 assert!(matches!(params.prefilter, PrefilterMode::None));
527 }
528
529 #[test]
530 fn hq_explicit_none_prefilter_is_respected() {
531 let opts = DenoiserOptions::builder()
532 .algorithm(Algorithm::NlmeansHq(HqParams {
533 auto_strength: true,
534 noise_floor: true,
535 sigma_override: None,
536 temporal_confidence: true,
537 thsad_scale: 1.0,
538 sigma_scale: 1.0,
539 }))
540 .prefilter(PrefilterMode::None)
541 .build();
542 let params = opts.to_nlm_params();
543
544 assert!(matches!(params.prefilter, PrefilterMode::None));
545 }
546
547 #[test]
548 fn fast_unset_prefilter_defaults_to_none() {
549 let opts = DenoiserOptions::builder().algorithm(Algorithm::Nlmeans).build();
550 let params = opts.to_nlm_params();
551
552 assert!(matches!(params.prefilter, PrefilterMode::None));
553 }
554
555 #[test]
556 fn hq_unset_strength_defaults_to_hq_default_strength() {
557 let opts = DenoiserOptions::builder()
559 .algorithm(Algorithm::NlmeansHq(HqParams {
560 auto_strength: true,
561 noise_floor: true,
562 sigma_override: None,
563 temporal_confidence: true,
564 thsad_scale: 1.0,
565 sigma_scale: 1.0,
566 }))
567 .build();
568 let params = opts.to_nlm_params();
569
570 let expected = hq_default_strength(ChannelMode::Yuv, 0);
571 assert!((params.strength - expected).abs() < f32::EPSILON);
572 }
573
574 #[test]
575 fn hq_no_auto_strength_falls_back_to_the_legacy_absolute_default() {
576 let opts = DenoiserOptions::builder()
582 .algorithm(Algorithm::NlmeansHq(HqParams {
583 auto_strength: false,
584 noise_floor: true,
585 sigma_override: None,
586 temporal_confidence: true,
587 thsad_scale: 1.0,
588 sigma_scale: 1.0,
589 }))
590 .build();
591 let params = opts.to_nlm_params();
592
593 let expected = NlmParams::default().strength;
594 assert!(
595 (params.strength - expected).abs() < f32::EPSILON,
596 "expected the legacy absolute default {expected}, got {} (looks like the \
597 auto-strength multiplier table leaked through)",
598 params.strength
599 );
600 }
601
602 #[test]
603 fn hq_luma_r4_uses_measured_table_value() {
604 let opts = DenoiserOptions::builder()
605 .channel_mode(ChannelMode::Luma)
606 .mode(DenoisingMode::Temporal { radius: 4 })
607 .algorithm(Algorithm::NlmeansHq(HqParams::default()))
608 .build();
609 let params = opts.to_nlm_params();
610
611 assert!((params.strength - 0.35).abs() < f32::EPSILON);
612 }
613
614 #[test]
615 fn hq_chroma_r4_uses_measured_table_value() {
616 let opts = DenoiserOptions::builder()
617 .channel_mode(ChannelMode::Chroma)
618 .mode(DenoisingMode::Temporal { radius: 4 })
619 .algorithm(Algorithm::NlmeansHq(HqParams::default()))
620 .build();
621 let params = opts.to_nlm_params();
622
623 assert!((params.strength - 0.70).abs() < f32::EPSILON);
624 }
625
626 #[test]
627 fn hq_yuv_r8_uses_measured_table_value() {
628 let opts = DenoiserOptions::builder()
629 .channel_mode(ChannelMode::Yuv)
630 .mode(DenoisingMode::Temporal { radius: 8 })
631 .algorithm(Algorithm::NlmeansHq(HqParams::default()))
632 .build();
633 let params = opts.to_nlm_params();
634
635 assert!((params.strength - 0.30).abs() < f32::EPSILON);
636 }
637
638 #[test]
639 fn hq_spacial_mode_uses_radius_zero_table_values() {
640 for channels in [ChannelMode::Luma, ChannelMode::Chroma, ChannelMode::Yuv] {
641 let opts = DenoiserOptions::builder()
642 .channel_mode(channels)
643 .mode(DenoisingMode::Spacial)
644 .algorithm(Algorithm::NlmeansHq(HqParams::default()))
645 .build();
646 let params = opts.to_nlm_params();
647
648 let expected = hq_default_strength(channels, 0);
649 assert!(
650 (params.strength - expected).abs() < f32::EPSILON,
651 "channels={channels:?}: expected {expected}, got {}",
652 params.strength
653 );
654 }
655 }
656
657 #[test]
658 fn hq_explicit_strength_wins_over_the_table_for_every_plane() {
659 for channels in [ChannelMode::Luma, ChannelMode::Chroma, ChannelMode::Yuv] {
660 let opts = DenoiserOptions::builder()
661 .channel_mode(channels)
662 .mode(DenoisingMode::Temporal { radius: 4 })
663 .algorithm(Algorithm::NlmeansHq(HqParams::default()))
664 .nlm(NlmTuning {
665 search_radius: None,
666 patch_radius: None,
667 strength: Some(0.99),
668 self_weight: None,
669 })
670 .build();
671 let params = opts.to_nlm_params();
672
673 assert!(
674 (params.strength - 0.99).abs() < f32::EPSILON,
675 "channels={channels:?}: explicit strength was overridden by the table"
676 );
677 }
678 }
679
680 #[test]
681 fn hq_explicit_strength_is_respected() {
682 let opts = DenoiserOptions::builder()
683 .algorithm(Algorithm::NlmeansHq(HqParams {
684 auto_strength: true,
685 noise_floor: true,
686 sigma_override: None,
687 temporal_confidence: true,
688 thsad_scale: 1.0,
689 sigma_scale: 1.0,
690 }))
691 .nlm(NlmTuning {
692 search_radius: None,
693 patch_radius: None,
694 strength: Some(1.0),
695 self_weight: None,
696 })
697 .build();
698 let params = opts.to_nlm_params();
699
700 assert!((params.strength - 1.0).abs() < f32::EPSILON);
701 }
702
703 #[test]
704 fn fast_unset_strength_defaults_to_legacy_default() {
705 let opts = DenoiserOptions::builder().algorithm(Algorithm::Nlmeans).build();
706 let params = opts.to_nlm_params();
707
708 assert!((params.strength - 1.2).abs() < f32::EPSILON);
709 }
710
711 #[test]
712 fn motion_compensation_passthrough() {
713 let opts = DenoiserOptions::builder()
714 .mode(DenoisingMode::Temporal { radius: 1 })
715 .motion_compensation(MotionCompensationMode::Mvtools {
716 blksize: 16,
717 overlap: 8,
718 search_radius: 4,
719 pyramid_levels: 2,
720 estimation: MotionEstimation::Direct,
721 })
722 .build();
723 let params = opts.to_nlm_params();
724
725 assert!(matches!(
726 params.motion_compensation,
727 MotionCompensationMode::Mvtools {
728 blksize: 16,
729 overlap: 8,
730 search_radius: 4,
731 pyramid_levels: 2,
732 ..
733 }
734 ));
735 }
736
737 #[test]
738 fn motion_compensation_defaults_to_none() {
739 let opts = DenoiserOptions::builder().build();
740 let params = opts.to_nlm_params();
741 assert!(matches!(params.motion_compensation, MotionCompensationMode::None));
742 }
743
744 #[test]
745 fn nlm_tuning_overrides_individual_fields() {
746 let defaults = NlmParams::default();
747 let opts = DenoiserOptions::builder()
748 .nlm(NlmTuning {
749 search_radius: Some(7),
750 patch_radius: None,
751 strength: Some(2.5),
752 self_weight: None,
753 })
754 .build();
755 let params = opts.to_nlm_params();
756
757 assert_eq!(params.search_radius, 7);
758 assert_eq!(params.patch_radius, defaults.patch_radius);
759 assert!((params.strength - 2.5).abs() < f32::EPSILON);
760 assert!((params.self_weight - defaults.self_weight).abs() < f32::EPSILON);
761 }
762}
763
764#[cfg(all(test, feature = "vulkan"))]
765mod tests {
766 use super::*;
767
768 fn opts(mode: DenoisingMode) -> DenoiserOptions {
769 DenoiserOptions::builder()
770 .channel_mode(ChannelMode::Luma)
771 .mode(mode)
772 .build()
773 }
774
775 fn frame(w: u32, h: u32) -> Vec<f32> {
776 vec![0.5f32; (w * h) as usize]
777 }
778
779 #[test]
780 fn spatial_denoise_roundtrip() {
781 let mut d = Denoiser::create(
782 &[Accelerator::Vulkan],
783 &Device::Default,
784 16,
785 16,
786 opts(DenoisingMode::Spacial),
787 )
788 .expect("denoiser construction failed");
789 assert_eq!(d.selected_accelerator(), Accelerator::Vulkan);
790
791 d.push_frame(&frame(16, 16)).expect("push failed");
792 let out = d.recv_frame().expect("recv failed").expect("no frame");
793 assert_eq!(out.len(), 16 * 16);
794 }
795
796 #[test]
797 fn invalid_params_surface_as_error() {
798 let bad = DenoiserOptions::builder()
799 .nlm(NlmTuning {
800 search_radius: None,
801 patch_radius: None,
802 strength: Some(0.0),
803 self_weight: None,
804 })
805 .build();
806 let result = Denoiser::create(&[Accelerator::Vulkan], &Device::Default, 16, 16, bad);
807
808 match result {
809 Err(DenoiserError::Other(_)) => {},
810 Err(other) => panic!("expected DenoiserError::Other, got {other:?}"),
811 Ok(_) => panic!("expected validation error, got Ok"),
812 }
813 }
814
815 #[test]
816 fn tiny_frame_dimensions_surface_as_error() {
817 let result = Denoiser::create(
818 &[Accelerator::Vulkan],
819 &Device::Default,
820 2,
821 2,
822 opts(DenoisingMode::Spacial),
823 );
824
825 match result {
826 Err(DenoiserError::Other(e)) => {
827 assert!(
828 e.to_string().contains("supported minimum"),
829 "unexpected error message: {e}"
830 );
831 },
832 Err(other) => panic!("expected DenoiserError::Other, got {other:?}"),
833 Ok(_) => panic!("expected dimension validation error, got Ok"),
834 }
835 }
836
837 #[test]
838 fn push_after_pending_returns_queue_full() {
839 let mut d = Denoiser::create(
840 &[Accelerator::Vulkan],
841 &Device::Default,
842 16,
843 16,
844 opts(DenoisingMode::Spacial),
845 )
846 .unwrap();
847
848 d.push_frame(&frame(16, 16)).unwrap();
852 d.push_frame(&frame(16, 16)).unwrap();
853 let err = d.push_frame(&frame(16, 16)).expect_err("expected QueueFull");
854 assert!(matches!(err, DenoiserError::QueueFull));
855
856 let out = d.recv_frame().unwrap().unwrap();
857 assert_eq!(out.len(), 16 * 16);
858
859 d.push_frame(&frame(16, 16)).expect("push after drain failed");
861 }
862
863 fn frame_filled(w: u32, h: u32, value: f32) -> Vec<f32> {
864 vec![value; (w * h) as usize]
865 }
866
867 fn push_n_with_drain(d: &mut Denoiser, n: usize, value: f32, out: &mut Vec<Vec<f32>>) {
870 for _ in 0..n {
871 loop {
872 match d.push_frame(&frame_filled(16, 16, value)) {
873 Ok(()) => break,
874 Err(DenoiserError::QueueFull) => {
875 let f = d
876 .recv_frame()
877 .expect("recv ok")
878 .expect("queue full but recv yielded none");
879 out.push(f);
880 },
881 Err(e) => panic!("unexpected push error: {e:?}"),
882 }
883 }
884 }
885 }
886
887 #[test]
888 fn flush_leaves_denoiser_reusable_spatial() {
889 let mut d = Denoiser::create(
890 &[Accelerator::Vulkan],
891 &Device::Default,
892 16,
893 16,
894 opts(DenoisingMode::Spacial),
895 )
896 .unwrap();
897
898 let mut batch_a = Vec::new();
899 push_n_with_drain(&mut d, 5, 0.25, &mut batch_a);
900 d.flush(|f| batch_a.push(f)).expect("first flush failed");
901 assert_eq!(batch_a.len(), 5);
902
903 assert!(d.recv_frame().unwrap().is_none());
905
906 let mut batch_b = Vec::new();
907 push_n_with_drain(&mut d, 5, 0.75, &mut batch_b);
908 d.flush(|f| batch_b.push(f)).expect("second flush failed");
909 assert_eq!(batch_b.len(), 5);
910
911 for v in batch_b.iter().flatten() {
912 assert!((v - 0.75).abs() < 0.1, "batch_b carried state from batch_a: {v}");
913 }
914 for v in batch_a.iter().flatten() {
915 assert!((v - 0.25).abs() < 0.1, "batch_a value unexpectedly drifted: {v}");
916 }
917 }
918
919 #[test]
920 fn flush_leaves_denoiser_reusable_temporal() {
921 let mut d = Denoiser::create(
922 &[Accelerator::Vulkan],
923 &Device::Default,
924 16,
925 16,
926 opts(DenoisingMode::Temporal { radius: 1 }),
927 )
928 .unwrap();
929
930 let mut batch_a = Vec::new();
931 push_n_with_drain(&mut d, 5, 0.25, &mut batch_a);
932 d.flush(|f| batch_a.push(f)).expect("first flush failed");
933 assert_eq!(batch_a.len(), 5, "expected 5 frames from first batch");
934
935 assert!(d.recv_frame().unwrap().is_none());
939 d.push_frame(&frame_filled(16, 16, 0.75)).unwrap();
940 assert!(
941 d.recv_frame().unwrap().is_none(),
942 "first push of new temporal stream should not produce output yet"
943 );
944
945 let mut batch_b = Vec::new();
947 push_n_with_drain(&mut d, 4, 0.75, &mut batch_b);
948 d.flush(|f| batch_b.push(f)).expect("second flush failed");
949 assert_eq!(batch_b.len(), 5, "expected 5 frames from second batch");
950
951 for v in batch_b.iter().flatten() {
952 assert!((v - 0.75).abs() < 0.1, "batch_b carried state from batch_a: {v}");
953 }
954 }
955
956 #[test]
957 fn flush_emits_exactly_n_outputs_for_small_n() {
958 for n in 1..=5usize {
963 let mut d = Denoiser::create(
964 &[Accelerator::Vulkan],
965 &Device::Default,
966 16,
967 16,
968 opts(DenoisingMode::Temporal { radius: 2 }),
969 )
970 .unwrap();
971
972 let mut out = Vec::new();
973 push_n_with_drain(&mut d, n, 0.5, &mut out);
974 d.flush(|f| out.push(f)).expect("flush failed");
975 assert_eq!(
976 out.len(),
977 n,
978 "expected {n} outputs for {n} pushes, got {}",
979 out.len()
980 );
981 }
982 }
983}
984
985#[cfg(all(test, feature = "cpu"))]
987mod cpu_smoke_tests {
988 use super::*;
989
990 #[test]
991 fn cpu_backend_denoises_a_frame() {
992 let opts = DenoiserOptions::builder()
993 .channel_mode(ChannelMode::Luma)
994 .mode(DenoisingMode::Spacial)
995 .build();
996 let mut d = Denoiser::create(&[Accelerator::Cpu], &Device::Default, 16, 16, opts)
997 .expect("denoiser construction failed");
998 assert_eq!(d.selected_accelerator(), Accelerator::Cpu);
999
1000 d.push_frame(&vec![0.5f32; 16 * 16]).expect("push failed");
1001 let out = d.recv_frame().expect("recv failed").expect("no frame");
1002 assert_eq!(out.len(), 16 * 16);
1003 }
1004}