1use std::collections::VecDeque;
2
3use cubecl::Runtime;
4use cubecl::prelude::ComputeClient;
5
6use crate::accelerate::Accelerator;
7use crate::device::Device;
8use crate::nl4d::{Nl4dDenoiser, Nl4dParams};
9#[cfg(test)]
10use crate::nlmeans::MotionEstimation;
11use crate::nlmeans::{
12 ChannelMode,
13 Depth,
14 HqParams,
15 MotionCompensationMode,
16 MotionSearch,
17 NlmDenoiser,
18 NlmParams,
19 Pending,
20 PrefilterMode,
21 TryWait,
22 hq_default_strength,
23 validate_dimensions,
24};
25use crate::sniff::sniff_best_accelerator;
26
27#[derive(Debug, Clone, bon::Builder)]
35pub struct DenoiserOptions {
36 #[builder(default = ChannelMode::Yuv)]
38 pub channel_mode: ChannelMode,
39 #[builder(default = DenoisingMode::Spacial)]
42 pub mode: DenoisingMode,
43 #[builder(default)]
46 pub algorithm: Algorithm,
47 #[builder(default = OutputFormat::F32)]
49 pub output_format: OutputFormat,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum OutputFormat {
60 F32,
62 Wire { depth: Depth },
64}
65
66#[derive(Debug, Clone, PartialEq)]
68pub enum FrameOutput {
69 F32(Vec<f32>),
71 Wire(Vec<u8>),
74}
75
76impl FrameOutput {
77 pub fn into_f32(self) -> Option<Vec<f32>> {
79 match self {
80 Self::F32(v) => Some(v),
81 Self::Wire(_) => None,
82 }
83 }
84
85 pub fn into_wire(self) -> Option<Vec<u8>> {
87 match self {
88 Self::Wire(v) => Some(v),
89 Self::F32(_) => None,
90 }
91 }
92
93 pub fn as_f32(&self) -> Option<&[f32]> {
96 match self {
97 Self::F32(v) => Some(v),
98 Self::Wire(_) => None,
99 }
100 }
101
102 pub fn as_wire(&self) -> Option<&[u8]> {
105 match self {
106 Self::Wire(v) => Some(v),
107 Self::F32(_) => None,
108 }
109 }
110}
111
112#[derive(Debug, Copy, Clone, PartialEq)]
117pub enum Algorithm {
118 Nlmeans(NlmeansOptions),
121 NlmeansHq(NlmeansHqOptions),
127 Nl4d(Nl4dOptions),
134}
135
136impl Default for Algorithm {
137 fn default() -> Self {
138 Self::Nlmeans(NlmeansOptions::default())
139 }
140}
141
142#[derive(Debug, Copy, Clone, Default, PartialEq)]
144pub struct NlmeansOptions {
145 pub prefilter: PrefilterMode,
150 pub motion_compensation: MotionCompensationMode,
159 pub tuning: NlmTuning,
162}
163
164#[derive(Debug, Copy, Clone, Default, PartialEq)]
166pub struct NlmeansHqOptions {
167 pub nlm: NlmeansOptions,
169 pub hq: HqParams,
171}
172
173#[derive(Debug, Copy, Clone, PartialEq)]
191pub struct Nl4dOptions {
192 pub motion: MotionSearch,
194 pub sigma: Option<f32>,
200 pub sigma_scale: f32,
206 pub thsad_scale: f32,
212 pub refine: u32,
215 pub spatial_radius: u32,
218 pub lambda_ht: Option<f32>,
224 pub lambda_ht_scale: f32,
231 pub c_min: f32,
236 pub mismatch_scale: f32,
243 pub confidence_variance: bool,
247 pub kaiser_beta: f32,
251 pub windowed_noise_estimation: bool,
261 pub field_lambda: f32,
263}
264
265impl Default for Nl4dOptions {
266 fn default() -> Self {
267 let defaults = Nl4dParams::default();
268 let hq = HqParams::default();
269 Self {
270 motion: MotionSearch::default(),
271 sigma: hq.sigma_override,
272 sigma_scale: hq.sigma_scale,
273 thsad_scale: hq.thsad_scale,
274 refine: defaults.refine,
275 spatial_radius: defaults.spatial_radius,
276 lambda_ht: None,
280 lambda_ht_scale: 1.0,
281 c_min: defaults.c_min,
282 mismatch_scale: defaults.mismatch_scale,
283 confidence_variance: defaults.confidence_variance,
284 kaiser_beta: defaults.kaiser_beta,
285 windowed_noise_estimation: false,
286 field_lambda: defaults.field_lambda,
287 }
288 }
289}
290
291impl Nl4dOptions {
292 fn to_hq_params(self) -> HqParams {
299 HqParams {
300 sigma_override: self.sigma,
301 sigma_scale: self.sigma_scale,
302 thsad_scale: self.thsad_scale,
303 temporal_confidence: true,
304 windowed_noise_estimation: self.windowed_noise_estimation,
305 ..HqParams::default()
306 }
307 }
308}
309
310pub fn nl4d_default_lambda_ht(channels: ChannelMode) -> f32 {
330 match channels {
331 ChannelMode::Luma | ChannelMode::Yuv => 5.2,
332 ChannelMode::Chroma => 3.4,
333 }
334}
335
336fn resolve_lambda_ht(opts: &Nl4dOptions, channels: ChannelMode) -> Result<f32, String> {
348 if !(opts.lambda_ht_scale.is_finite() && (0.1..=10.0).contains(&opts.lambda_ht_scale)) {
349 return Err(format!(
350 "lambda_ht_scale must be finite and in [0.1, 10.0], got {}",
351 opts.lambda_ht_scale
352 ));
353 }
354
355 let lambda_ht = opts.lambda_ht.unwrap_or_else(|| nl4d_default_lambda_ht(channels));
356
357 Ok(lambda_ht * opts.lambda_ht_scale)
358}
359
360#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, strum_macros::EnumString)]
371#[strum(ascii_case_insensitive)]
372pub enum Preset {
373 Veryfast,
375 Fast,
377 #[default]
379 Base,
380 Slow,
382 Veryslow,
384}
385
386#[derive(Debug, Copy, Clone, PartialEq, Eq, strum_macros::EnumString)]
388#[strum(ascii_case_insensitive)]
389pub enum NlmeansVariant {
390 Fast,
392 Hq,
395}
396
397pub fn nlmeans_variant_for(preset: Preset) -> NlmeansVariant {
399 match preset {
400 Preset::Veryfast => NlmeansVariant::Fast,
401 Preset::Fast | Preset::Base | Preset::Slow | Preset::Veryslow => NlmeansVariant::Hq,
402 }
403}
404
405pub fn nlmeans_temporal_radius_for(preset: Preset) -> u32 {
408 match preset {
409 Preset::Veryfast => 0,
410 Preset::Fast => 1,
411 Preset::Base => 2,
412 Preset::Slow => 4,
413 Preset::Veryslow => 8,
414 }
415}
416
417pub fn nlmeans_search_radius_for(preset: Preset) -> u32 {
420 match preset {
421 Preset::Veryfast | Preset::Fast | Preset::Base => 2,
422 Preset::Slow | Preset::Veryslow => 4,
423 }
424}
425
426pub fn nl4d_temporal_radius_for(preset: Preset) -> u32 {
432 match preset {
433 Preset::Veryfast | Preset::Fast => 1,
434 Preset::Base => 2,
435 Preset::Slow => 4,
436 Preset::Veryslow => 8,
437 }
438}
439
440pub fn nl4d_spatial_radius_for(preset: Preset) -> u32 {
451 match preset {
452 Preset::Veryfast => 6,
453 Preset::Fast | Preset::Base | Preset::Slow | Preset::Veryslow => {
454 Nl4dOptions::default().spatial_radius
455 },
456 }
457}
458
459#[derive(Debug, Copy, Clone, Eq, PartialEq)]
461pub enum DenoisingMode {
462 Spacial,
464 Temporal { radius: u32 },
466}
467
468#[derive(Debug, Copy, Clone, Default, PartialEq)]
473pub struct NlmTuning {
474 pub search_radius: Option<u32>,
475 pub patch_radius: Option<u32>,
476 pub strength: Option<f32>,
477 pub self_weight: Option<f32>,
478}
479
480impl DenoiserOptions {
481 #[doc(hidden)]
495 pub fn to_nlm_params(&self) -> NlmParams {
496 let temporal_radius = match self.mode {
497 DenoisingMode::Spacial => 0,
498 DenoisingMode::Temporal { radius } => radius,
499 };
500
501 match self.algorithm {
502 Algorithm::Nlmeans(opts) => self.nlm_params_for(opts, None, temporal_radius),
503 Algorithm::NlmeansHq(opts) => self.nlm_params_for(opts.nlm, Some(opts.hq), temporal_radius),
504 Algorithm::Nl4d(opts) => NlmParams {
508 channels: self.channel_mode,
509 motion_compensation: opts.motion.into(),
510 temporal_radius,
511 hq: Some(opts.to_hq_params()),
512 ..NlmParams::default()
513 },
514 }
515 }
516
517 fn nlm_params_for(&self, opts: NlmeansOptions, hq: Option<HqParams>, temporal_radius: u32) -> NlmParams {
520 let strength = opts.tuning.strength.unwrap_or(match hq {
536 Some(hq) if hq.auto_strength => hq_default_strength(self.channel_mode, temporal_radius),
537 _ => NlmParams::default().strength,
538 });
539
540 let defaults = NlmParams::default();
541 NlmParams {
542 channels: self.channel_mode,
543 prefilter: opts.prefilter,
544 motion_compensation: opts.motion_compensation,
545 temporal_radius,
546 hq,
547 strength,
548 search_radius: opts.tuning.search_radius.unwrap_or(defaults.search_radius),
549 patch_radius: opts.tuning.patch_radius.unwrap_or(defaults.patch_radius),
550 self_weight: opts.tuning.self_weight.unwrap_or(defaults.self_weight),
551 }
552 }
553}
554
555#[derive(Debug, thiserror::Error)]
557pub enum DenoiserError {
558 #[error("denoiser queue is full, collect the pending frame before pushing more")]
564 QueueFull,
565 #[error("denoiser failed earlier, reset the stream before using it again")]
573 Poisoned,
574 #[error("no accelerator from the priority list is available")]
576 NoAcceleratorAvailable,
577 #[error(transparent)]
580 Other(#[from] anyhow::Error),
581}
582
583enum Engine<R: Runtime> {
589 Nlm(Box<NlmDenoiser<R>>),
590 Nl4d(Box<Nl4dDenoiser<R>>),
591}
592
593impl<R: Runtime> Engine<R> {
594 fn is_nl4d(&self) -> bool {
595 matches!(self, Self::Nl4d(_))
596 }
597
598 fn push_frame(&mut self, frame: &[f32]) {
599 match self {
600 Self::Nlm(d) => d.push_frame(frame),
601 Self::Nl4d(d) => d.push_frame(frame),
602 }
603 }
604
605 fn push_frame_wire(&mut self, planes: &[&[u8]], depth: Depth) {
606 match self {
607 Self::Nlm(d) => d.push_frame_wire(planes, depth),
608 Self::Nl4d(d) => d.push_frame_wire(planes, depth),
609 }
610 }
611
612 fn denoise_submit(&mut self) -> Result<Option<Pending<R>>, anyhow::Error> {
613 match self {
614 Self::Nlm(d) => d.denoise_submit(),
615 Self::Nl4d(d) => d.denoise_submit().map_err(anyhow::Error::from),
620 }
621 }
622
623 #[cfg(test)]
624 fn wire_outputs(&self) -> Option<&[cubecl::server::Handle; 2]> {
625 match self {
626 Self::Nlm(d) => d.wire_outputs_for_test(),
627 Self::Nl4d(d) => d.wire_outputs_for_test(),
628 }
629 }
630
631 fn flush(&mut self, sink: impl FnMut(&FrameOutput)) -> Result<(), anyhow::Error> {
632 match self {
633 Self::Nlm(d) => d.flush(sink),
634 Self::Nl4d(d) => d.flush(sink).map_err(anyhow::Error::from),
635 }
636 }
637
638 fn reset_stream(&mut self) {
639 match self {
640 Self::Nlm(d) => d.reset_stream_state(),
641 Self::Nl4d(d) => d.reset_stream(),
642 }
643 }
644}
645
646fn build_engine<R: Runtime>(
656 client: &ComputeClient<R>,
657 algorithm: &Algorithm,
658 params: NlmParams,
659 width: u32,
660 height: u32,
661 output_format: OutputFormat,
662) -> Result<Engine<R>, DenoiserError> {
663 match algorithm {
664 Algorithm::Nl4d(opts) => {
665 if params.temporal_radius == 0 {
668 return Err(DenoiserError::Other(anyhow::anyhow!(
669 "nl4d needs a temporal window, set DenoiserOptions::mode to \
670 DenoisingMode::Temporal"
671 )));
672 }
673
674 let lambda_ht = resolve_lambda_ht(opts, params.channels)
675 .map_err(|e| DenoiserError::Other(anyhow::anyhow!(e)))?;
676 let nl4d_params = Nl4dParams {
677 temporal_radius: params.temporal_radius,
678 nlm: params,
679 refine: opts.refine,
680 spatial_radius: opts.spatial_radius,
681 lambda_ht,
682 c_min: opts.c_min,
683 mismatch_scale: opts.mismatch_scale,
684 confidence_variance: opts.confidence_variance,
685 kaiser_beta: opts.kaiser_beta,
686 field_lambda: opts.field_lambda,
687 };
688 let denoiser =
689 Nl4dDenoiser::with_output_format(client, nl4d_params, width, height, output_format)
690 .map_err(|e| DenoiserError::Other(anyhow::anyhow!(e)))?;
691 Ok(Engine::Nl4d(Box::new(denoiser)))
692 },
693 Algorithm::Nlmeans(_) | Algorithm::NlmeansHq(_) => Ok(Engine::Nlm(Box::new(
694 NlmDenoiser::with_output_format(client, params, width, height, output_format),
695 ))),
696 }
697}
698
699enum Backend {
700 #[cfg(feature = "cuda")]
701 Cuda(Engine<cubecl::cuda::CudaRuntime>),
702 #[cfg(feature = "rocm")]
703 Rocm(Engine<cubecl::hip::HipRuntime>),
704 #[cfg(any(feature = "vulkan", feature = "metal"))]
705 Wgpu(Engine<cubecl::wgpu::WgpuRuntime>),
706}
707
708impl Backend {
709 fn is_nl4d(&self) -> bool {
710 match self {
711 #[cfg(feature = "cuda")]
712 Self::Cuda(e) => e.is_nl4d(),
713 #[cfg(feature = "rocm")]
714 Self::Rocm(e) => e.is_nl4d(),
715 #[cfg(any(feature = "vulkan", feature = "metal"))]
716 Self::Wgpu(e) => e.is_nl4d(),
717 }
718 }
719
720 #[cfg(test)]
721 fn wire_outputs(&self) -> Option<&[cubecl::server::Handle; 2]> {
722 match self {
723 #[cfg(feature = "cuda")]
724 Self::Cuda(e) => e.wire_outputs(),
725 #[cfg(feature = "rocm")]
726 Self::Rocm(e) => e.wire_outputs(),
727 #[cfg(any(feature = "vulkan", feature = "metal"))]
728 Self::Wgpu(e) => e.wire_outputs(),
729 }
730 }
731}
732
733enum BackendPending {
734 #[cfg(feature = "cuda")]
735 Cuda(Pending<cubecl::cuda::CudaRuntime>),
736 #[cfg(feature = "rocm")]
737 Rocm(Pending<cubecl::hip::HipRuntime>),
738 #[cfg(any(feature = "vulkan", feature = "metal"))]
739 Wgpu(Pending<cubecl::wgpu::WgpuRuntime>),
740}
741
742impl BackendPending {
743 fn wait(self) -> Result<FrameOutput, anyhow::Error> {
744 match self {
745 #[cfg(feature = "cuda")]
746 Self::Cuda(p) => p.wait(),
747 #[cfg(feature = "rocm")]
748 Self::Rocm(p) => p.wait(),
749 #[cfg(any(feature = "vulkan", feature = "metal"))]
750 Self::Wgpu(p) => p.wait(),
751 }
752 }
753
754 fn try_wait(self) -> Result<Result<FrameOutput, Self>, anyhow::Error> {
757 match self {
758 #[cfg(feature = "cuda")]
759 Self::Cuda(p) => match p.try_wait()? {
760 TryWait::Ready(frame) => Ok(Ok(frame)),
761 TryWait::NotReady(p) => Ok(Err(Self::Cuda(p))),
762 },
763 #[cfg(feature = "rocm")]
764 Self::Rocm(p) => match p.try_wait()? {
765 TryWait::Ready(frame) => Ok(Ok(frame)),
766 TryWait::NotReady(p) => Ok(Err(Self::Rocm(p))),
767 },
768 #[cfg(any(feature = "vulkan", feature = "metal"))]
769 Self::Wgpu(p) => match p.try_wait()? {
770 TryWait::Ready(frame) => Ok(Ok(frame)),
771 TryWait::NotReady(p) => Ok(Err(Self::Wgpu(p))),
772 },
773 }
774 }
775}
776
777pub const MAX_PENDING: usize = 2;
784
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
796pub struct WindowSpan {
797 pub behind: usize,
799 pub ahead: usize,
801}
802
803impl WindowSpan {
804 pub fn frame_count(&self) -> usize {
807 self.behind + 1 + self.ahead
808 }
809}
810
811pub struct Denoiser {
862 backend: Backend,
863 pending: VecDeque<BackendPending>,
864 accelerator: Accelerator,
865 width: u32,
866 height: u32,
867 temporal_radius: u32,
868 output_format: OutputFormat,
869 frames_pushed: u32,
870 poisoned: bool,
876}
877
878impl Denoiser {
879 pub fn create(
899 accelerators: &[Accelerator],
900 device: &Device,
901 width: u32,
902 height: u32,
903 options: DenoiserOptions,
904 ) -> Result<Self, DenoiserError> {
905 let accelerator =
906 sniff_best_accelerator(accelerators, device).ok_or(DenoiserError::NoAcceleratorAvailable)?;
907
908 let params = options.to_nlm_params();
909 params.validate()?;
910 validate_dimensions(width, height)?;
911
912 let temporal_radius = params.temporal_radius;
913 let backend = build_backend(
914 accelerator,
915 device,
916 &options.algorithm,
917 params,
918 width,
919 height,
920 options.output_format,
921 )?;
922
923 Ok(Self {
924 backend,
925 pending: VecDeque::with_capacity(MAX_PENDING),
926 accelerator,
927 width,
928 height,
929 temporal_radius,
930 output_format: options.output_format,
931 frames_pushed: 0,
932 poisoned: false,
933 })
934 }
935
936 pub fn selected_accelerator(&self) -> Accelerator {
938 self.accelerator
939 }
940
941 pub fn width(&self) -> u32 {
943 self.width
944 }
945
946 pub fn height(&self) -> u32 {
948 self.height
949 }
950
951 pub fn temporal_radius(&self) -> u32 {
953 self.temporal_radius
954 }
955
956 pub fn output_format(&self) -> OutputFormat {
958 self.output_format
959 }
960
961 pub fn window_span(&self) -> WindowSpan {
979 let radius = self.temporal_radius as usize;
980 let span = if self.backend.is_nl4d() {
981 2 * radius
982 } else {
983 radius
984 };
985 WindowSpan {
986 behind: span,
987 ahead: span,
988 }
989 }
990
991 pub fn push_frame(&mut self, frame: &[f32]) -> Result<(), DenoiserError> {
1010 if self.poisoned {
1011 return Err(DenoiserError::Poisoned);
1012 }
1013 self.push_frame_inner(frame).inspect_err(|err| {
1014 if !matches!(err, DenoiserError::QueueFull) {
1015 self.poisoned = true;
1016 }
1017 })
1018 }
1019
1020 fn push_frame_inner(&mut self, frame: &[f32]) -> Result<(), DenoiserError> {
1021 let window_full = self.frames_pushed > self.temporal_radius;
1025 if window_full && self.pending.len() >= MAX_PENDING {
1026 return Err(DenoiserError::QueueFull);
1027 }
1028
1029 match &mut self.backend {
1030 #[cfg(feature = "cuda")]
1031 Backend::Cuda(d) => {
1032 d.push_frame(frame);
1033 if let Some(p) = d.denoise_submit()? {
1034 self.pending.push_back(BackendPending::Cuda(p));
1035 }
1036 },
1037 #[cfg(feature = "rocm")]
1038 Backend::Rocm(d) => {
1039 d.push_frame(frame);
1040 if let Some(p) = d.denoise_submit()? {
1041 self.pending.push_back(BackendPending::Rocm(p));
1042 }
1043 },
1044 #[cfg(any(feature = "vulkan", feature = "metal"))]
1045 Backend::Wgpu(d) => {
1046 d.push_frame(frame);
1047 if let Some(p) = d.denoise_submit()? {
1048 self.pending.push_back(BackendPending::Wgpu(p));
1049 }
1050 },
1051 }
1052
1053 self.frames_pushed = self.frames_pushed.saturating_add(1);
1054 Ok(())
1055 }
1056
1057 pub fn push_frame_wire(&mut self, planes: &[&[u8]], depth: Depth) -> Result<(), DenoiserError> {
1066 if self.poisoned {
1067 return Err(DenoiserError::Poisoned);
1068 }
1069 self.push_frame_wire_inner(planes, depth).inspect_err(|err| {
1070 if !matches!(err, DenoiserError::QueueFull) {
1071 self.poisoned = true;
1072 }
1073 })
1074 }
1075
1076 fn push_frame_wire_inner(&mut self, planes: &[&[u8]], depth: Depth) -> Result<(), DenoiserError> {
1077 let window_full = self.frames_pushed > self.temporal_radius;
1079 if window_full && self.pending.len() >= MAX_PENDING {
1080 return Err(DenoiserError::QueueFull);
1081 }
1082
1083 match &mut self.backend {
1084 #[cfg(feature = "cuda")]
1085 Backend::Cuda(d) => {
1086 d.push_frame_wire(planes, depth);
1087 if let Some(p) = d.denoise_submit()? {
1088 self.pending.push_back(BackendPending::Cuda(p));
1089 }
1090 },
1091 #[cfg(feature = "rocm")]
1092 Backend::Rocm(d) => {
1093 d.push_frame_wire(planes, depth);
1094 if let Some(p) = d.denoise_submit()? {
1095 self.pending.push_back(BackendPending::Rocm(p));
1096 }
1097 },
1098 #[cfg(any(feature = "vulkan", feature = "metal"))]
1099 Backend::Wgpu(d) => {
1100 d.push_frame_wire(planes, depth);
1101 if let Some(p) = d.denoise_submit()? {
1102 self.pending.push_back(BackendPending::Wgpu(p));
1103 }
1104 },
1105 }
1106
1107 self.frames_pushed = self.frames_pushed.saturating_add(1);
1108 Ok(())
1109 }
1110
1111 pub fn push_frame_wire_priming(&mut self, planes: &[&[u8]], depth: Depth) -> Result<(), DenoiserError> {
1116 if self.poisoned {
1117 return Err(DenoiserError::Poisoned);
1118 }
1119 match &mut self.backend {
1120 #[cfg(feature = "cuda")]
1121 Backend::Cuda(d) => d.push_frame_wire(planes, depth),
1122 #[cfg(feature = "rocm")]
1123 Backend::Rocm(d) => d.push_frame_wire(planes, depth),
1124 #[cfg(any(feature = "vulkan", feature = "metal"))]
1125 Backend::Wgpu(d) => d.push_frame_wire(planes, depth),
1126 }
1127
1128 self.frames_pushed = self.frames_pushed.saturating_add(1);
1129 Ok(())
1130 }
1131
1132 pub fn push_frame_priming(&mut self, frame: &[f32]) -> Result<(), DenoiserError> {
1144 if self.poisoned {
1145 return Err(DenoiserError::Poisoned);
1146 }
1147 match &mut self.backend {
1148 #[cfg(feature = "cuda")]
1149 Backend::Cuda(d) => d.push_frame(frame),
1150 #[cfg(feature = "rocm")]
1151 Backend::Rocm(d) => d.push_frame(frame),
1152 #[cfg(any(feature = "vulkan", feature = "metal"))]
1153 Backend::Wgpu(d) => d.push_frame(frame),
1154 }
1155
1156 self.frames_pushed = self.frames_pushed.saturating_add(1);
1157 Ok(())
1158 }
1159
1160 pub fn reset_stream(&mut self) {
1173 self.pending.clear();
1174 self.frames_pushed = 0;
1175 self.poisoned = false;
1176
1177 match &mut self.backend {
1178 #[cfg(feature = "cuda")]
1179 Backend::Cuda(d) => d.reset_stream(),
1180 #[cfg(feature = "rocm")]
1181 Backend::Rocm(d) => d.reset_stream(),
1182 #[cfg(any(feature = "vulkan", feature = "metal"))]
1183 Backend::Wgpu(d) => d.reset_stream(),
1184 }
1185 }
1186
1187 pub fn recv_frame(&mut self) -> Result<Option<FrameOutput>, DenoiserError> {
1196 if self.poisoned {
1197 return Err(DenoiserError::Poisoned);
1198 }
1199 self.recv_frame_inner().inspect_err(|_| self.poisoned = true)
1200 }
1201
1202 fn recv_frame_inner(&mut self) -> Result<Option<FrameOutput>, DenoiserError> {
1203 let Some(pending) = self.pending.pop_front() else {
1204 return Ok(None);
1205 };
1206 Ok(Some(pending.wait()?))
1207 }
1208
1209 pub fn try_recv_frame(&mut self) -> Result<Option<FrameOutput>, DenoiserError> {
1231 if self.poisoned {
1232 return Err(DenoiserError::Poisoned);
1233 }
1234 self.try_recv_frame_inner().inspect_err(|_| self.poisoned = true)
1235 }
1236
1237 fn try_recv_frame_inner(&mut self) -> Result<Option<FrameOutput>, DenoiserError> {
1238 let Some(pending) = self.pending.pop_front() else {
1239 return Ok(None);
1240 };
1241
1242 match pending.try_wait()? {
1243 Ok(frame) => Ok(Some(frame)),
1244 Err(pending) => {
1245 self.pending.push_front(pending);
1246 Ok(None)
1247 },
1248 }
1249 }
1250
1251 pub fn flush(&mut self, sink: impl FnMut(FrameOutput)) -> Result<(), DenoiserError> {
1264 if self.poisoned {
1265 return Err(DenoiserError::Poisoned);
1266 }
1267 self.flush_inner(sink).inspect_err(|_| self.poisoned = true)
1268 }
1269
1270 fn flush_inner(&mut self, mut sink: impl FnMut(FrameOutput)) -> Result<(), DenoiserError> {
1271 while let Some(frame) = self.recv_frame_inner()? {
1276 sink(frame);
1277 }
1278
1279 match &mut self.backend {
1283 #[cfg(feature = "cuda")]
1284 Backend::Cuda(d) => d.flush(|frame| sink(frame.clone()))?,
1285 #[cfg(feature = "rocm")]
1286 Backend::Rocm(d) => d.flush(|frame| sink(frame.clone()))?,
1287 #[cfg(any(feature = "vulkan", feature = "metal"))]
1288 Backend::Wgpu(d) => d.flush(|frame| sink(frame.clone()))?,
1289 }
1290
1291 self.frames_pushed = 0;
1295
1296 Ok(())
1297 }
1298
1299 #[cfg(test)]
1303 pub(crate) fn poison_for_test(&mut self) {
1304 self.poisoned = true;
1305 }
1306
1307 #[cfg(test)]
1310 pub(crate) fn wire_outputs_for_test(&self) -> Option<&[cubecl::server::Handle; 2]> {
1311 self.backend.wire_outputs()
1312 }
1313}
1314
1315fn build_backend(
1316 accel: Accelerator,
1317 device: &Device,
1318 algorithm: &Algorithm,
1319 params: NlmParams,
1320 width: u32,
1321 height: u32,
1322 output_format: OutputFormat,
1323) -> Result<Backend, DenoiserError> {
1324 match accel {
1325 #[cfg(feature = "cuda")]
1326 Accelerator::Cuda => {
1327 let dev = device.to_cuda()?;
1328 let client = <cubecl::cuda::CudaRuntime as Runtime>::client(&dev);
1329 Ok(Backend::Cuda(build_engine(
1330 &client,
1331 algorithm,
1332 params,
1333 width,
1334 height,
1335 output_format,
1336 )?))
1337 },
1338 #[cfg(feature = "rocm")]
1339 Accelerator::Rocm => {
1340 let dev = device.to_amd()?;
1341 let client = <cubecl::hip::HipRuntime as Runtime>::client(&dev);
1342 Ok(Backend::Rocm(build_engine(
1343 &client,
1344 algorithm,
1345 params,
1346 width,
1347 height,
1348 output_format,
1349 )?))
1350 },
1351 #[cfg(feature = "vulkan")]
1352 Accelerator::Vulkan => {
1353 let dev = device.to_wgpu()?;
1354 let client = <cubecl::wgpu::WgpuRuntime as Runtime>::client(&dev);
1355 Ok(Backend::Wgpu(build_engine(
1356 &client,
1357 algorithm,
1358 params,
1359 width,
1360 height,
1361 output_format,
1362 )?))
1363 },
1364 #[cfg(feature = "metal")]
1365 Accelerator::Metal => {
1366 let dev = device.to_wgpu()?;
1367 let client = <cubecl::wgpu::WgpuRuntime as Runtime>::client(&dev);
1368 Ok(Backend::Wgpu(build_engine(
1369 &client,
1370 algorithm,
1371 params,
1372 width,
1373 height,
1374 output_format,
1375 )?))
1376 },
1377 #[cfg(docsrs)]
1381 #[expect(
1382 unreachable_patterns,
1383 reason = "the arm only keeps the match exhaustive on docs.rs"
1384 )]
1385 _ => unreachable!(),
1386 }
1387}
1388
1389#[cfg(test)]
1390mod options_tests {
1391 use super::*;
1392
1393 fn hq(hq: HqParams) -> Algorithm {
1396 Algorithm::NlmeansHq(NlmeansHqOptions {
1397 hq,
1398 ..NlmeansHqOptions::default()
1399 })
1400 }
1401
1402 fn fast_tuned(tuning: NlmTuning) -> Algorithm {
1404 Algorithm::Nlmeans(NlmeansOptions {
1405 tuning,
1406 ..NlmeansOptions::default()
1407 })
1408 }
1409
1410 #[test]
1411 fn nl4d_default_lambda_ht_differs_between_luma_and_chroma() {
1412 let luma = nl4d_default_lambda_ht(ChannelMode::Luma);
1413 let chroma = nl4d_default_lambda_ht(ChannelMode::Chroma);
1414
1415 assert!((luma - 5.2).abs() < f32::EPSILON);
1416 assert!((chroma - 3.4).abs() < f32::EPSILON);
1417 assert!(
1418 (chroma - luma).abs() > f32::EPSILON,
1419 "the two planes should not resolve to the same default"
1420 );
1421 }
1422
1423 #[test]
1424 fn nl4d_default_lambda_ht_yuv_reads_the_luma_value() {
1425 let yuv = nl4d_default_lambda_ht(ChannelMode::Yuv);
1426 let luma = nl4d_default_lambda_ht(ChannelMode::Luma);
1427
1428 assert!((yuv - luma).abs() < f32::EPSILON);
1429 }
1430
1431 #[test]
1432 fn resolve_lambda_ht_unset_uses_the_per_plane_default() {
1433 let opts = Nl4dOptions::default();
1434
1435 let luma = resolve_lambda_ht(&opts, ChannelMode::Luma).expect("the default scale is in range");
1436 let chroma = resolve_lambda_ht(&opts, ChannelMode::Chroma).expect("the default scale is in range");
1437
1438 assert!((luma - 5.2).abs() < f32::EPSILON, "got {luma}");
1439 assert!((chroma - 3.4).abs() < f32::EPSILON, "got {chroma}");
1440 }
1441
1442 #[test]
1443 fn resolve_lambda_ht_explicit_value_overrides_every_plane() {
1444 let opts = Nl4dOptions {
1445 lambda_ht: Some(4.4),
1446 ..Nl4dOptions::default()
1447 };
1448
1449 for channels in [ChannelMode::Luma, ChannelMode::Chroma, ChannelMode::Yuv] {
1450 let got = resolve_lambda_ht(&opts, channels).expect("the default scale is in range");
1451 assert!(
1452 (got - 4.4).abs() < f32::EPSILON,
1453 "channels {channels:?} got {got}"
1454 );
1455 }
1456 }
1457
1458 #[test]
1459 fn resolve_lambda_ht_default_scale_leaves_the_value_alone() {
1460 let opts = Nl4dOptions::default();
1461
1462 for channels in [ChannelMode::Luma, ChannelMode::Chroma, ChannelMode::Yuv] {
1463 let got = resolve_lambda_ht(&opts, channels).expect("the default scale is in range");
1464 let want = nl4d_default_lambda_ht(channels);
1465 assert!(
1466 (got - want).abs() < f32::EPSILON,
1467 "channels {channels:?} got {got}"
1468 );
1469 }
1470 }
1471
1472 #[test]
1473 fn resolve_lambda_ht_scale_multiplies_the_per_plane_default() {
1474 let opts = Nl4dOptions {
1475 lambda_ht_scale: 1.1,
1476 ..Nl4dOptions::default()
1477 };
1478
1479 for channels in [ChannelMode::Luma, ChannelMode::Chroma, ChannelMode::Yuv] {
1480 let got = resolve_lambda_ht(&opts, channels).expect("1.1 is in range");
1481 let want = nl4d_default_lambda_ht(channels) * 1.1;
1482 assert!(
1483 (got - want).abs() < 1e-5,
1484 "channels {channels:?} got {got}, want {want}"
1485 );
1486 }
1487 }
1488
1489 #[test]
1492 fn resolve_lambda_ht_scale_multiplies_an_explicit_value() {
1493 let opts = Nl4dOptions {
1494 lambda_ht: Some(5.0),
1495 lambda_ht_scale: 0.9,
1496 ..Nl4dOptions::default()
1497 };
1498
1499 let got = resolve_lambda_ht(&opts, ChannelMode::Luma).expect("0.9 is in range");
1500 assert!((got - 4.5).abs() < 1e-5, "got {got}");
1501 }
1502
1503 #[test]
1504 fn resolve_lambda_ht_rejects_an_out_of_range_scale() {
1505 for bad in [0.0, -1.0, 0.05, 10.5, f32::NAN, f32::INFINITY] {
1506 let opts = Nl4dOptions {
1507 lambda_ht_scale: bad,
1508 ..Nl4dOptions::default()
1509 };
1510 let err = resolve_lambda_ht(&opts, ChannelMode::Luma).unwrap_err();
1511 assert!(
1512 err.contains("lambda_ht_scale"),
1513 "lambda_ht_scale={bad} should be rejected, got {err}"
1514 );
1515 }
1516 }
1517
1518 #[test]
1519 fn the_default_algorithm_is_the_fast_nlmeans_path() {
1520 let opts = DenoiserOptions::builder().build();
1521 assert_eq!(opts.algorithm, Algorithm::Nlmeans(NlmeansOptions::default()));
1522 }
1523
1524 #[test]
1525 fn spatial_mode_maps_to_zero_temporal_radius() {
1526 let opts = DenoiserOptions::builder()
1527 .channel_mode(ChannelMode::Yuv)
1528 .mode(DenoisingMode::Spacial)
1529 .build();
1530 let params = opts.to_nlm_params();
1531
1532 assert_eq!(params.temporal_radius, 0);
1533 assert_eq!(params.channels, ChannelMode::Yuv);
1534 }
1535
1536 #[test]
1537 fn temporal_mode_propagates_radius() {
1538 let opts = DenoiserOptions::builder()
1539 .mode(DenoisingMode::Temporal { radius: 3 })
1540 .build();
1541 let params = opts.to_nlm_params();
1542
1543 assert_eq!(params.temporal_radius, 3);
1544 }
1545
1546 #[test]
1547 fn prefilter_passthrough() {
1548 let opts = DenoiserOptions::builder()
1549 .algorithm(Algorithm::Nlmeans(NlmeansOptions {
1550 prefilter: PrefilterMode::Bilateral {
1551 sigma_s: 3.0,
1552 sigma_r: 0.02,
1553 },
1554 ..NlmeansOptions::default()
1555 }))
1556 .build();
1557 let params = opts.to_nlm_params();
1558
1559 assert!(matches!(params.prefilter, PrefilterMode::Bilateral { .. }));
1560 }
1561
1562 #[test]
1563 fn hq_unset_prefilter_defaults_to_none() {
1564 let opts = DenoiserOptions::builder()
1565 .algorithm(hq(HqParams::default()))
1566 .build();
1567 let params = opts.to_nlm_params();
1568
1569 assert!(matches!(params.prefilter, PrefilterMode::None));
1570 }
1571
1572 #[test]
1573 fn fast_unset_prefilter_defaults_to_none() {
1574 let opts = DenoiserOptions::builder()
1575 .algorithm(Algorithm::Nlmeans(NlmeansOptions::default()))
1576 .build();
1577 let params = opts.to_nlm_params();
1578
1579 assert!(matches!(params.prefilter, PrefilterMode::None));
1580 }
1581
1582 #[test]
1583 fn hq_unset_strength_defaults_to_hq_default_strength() {
1584 let opts = DenoiserOptions::builder()
1586 .algorithm(hq(HqParams::default()))
1587 .build();
1588 let params = opts.to_nlm_params();
1589
1590 let expected = hq_default_strength(ChannelMode::Yuv, 0);
1591 assert!((params.strength - expected).abs() < f32::EPSILON);
1592 }
1593
1594 #[test]
1595 fn hq_no_auto_strength_falls_back_to_the_legacy_absolute_default() {
1596 let opts = DenoiserOptions::builder()
1603 .algorithm(hq(HqParams {
1604 auto_strength: false,
1605 ..HqParams::default()
1606 }))
1607 .build();
1608 let params = opts.to_nlm_params();
1609
1610 let expected = NlmParams::default().strength;
1611 assert!(
1612 (params.strength - expected).abs() < f32::EPSILON,
1613 "expected the legacy absolute default {expected}, got {}, which looks like the \
1614 auto-strength multiplier table leaking through",
1615 params.strength
1616 );
1617 }
1618
1619 #[test]
1620 fn hq_luma_r4_uses_measured_table_value() {
1621 let opts = DenoiserOptions::builder()
1622 .channel_mode(ChannelMode::Luma)
1623 .mode(DenoisingMode::Temporal { radius: 4 })
1624 .algorithm(hq(HqParams::default()))
1625 .build();
1626 let params = opts.to_nlm_params();
1627
1628 assert!((params.strength - 0.35).abs() < f32::EPSILON);
1629 }
1630
1631 #[test]
1632 fn hq_chroma_r4_uses_measured_table_value() {
1633 let opts = DenoiserOptions::builder()
1634 .channel_mode(ChannelMode::Chroma)
1635 .mode(DenoisingMode::Temporal { radius: 4 })
1636 .algorithm(hq(HqParams::default()))
1637 .build();
1638 let params = opts.to_nlm_params();
1639
1640 assert!((params.strength - 0.70).abs() < f32::EPSILON);
1641 }
1642
1643 #[test]
1644 fn hq_yuv_r8_uses_measured_table_value() {
1645 let opts = DenoiserOptions::builder()
1646 .channel_mode(ChannelMode::Yuv)
1647 .mode(DenoisingMode::Temporal { radius: 8 })
1648 .algorithm(hq(HqParams::default()))
1649 .build();
1650 let params = opts.to_nlm_params();
1651
1652 assert!((params.strength - 0.30).abs() < f32::EPSILON);
1653 }
1654
1655 #[test]
1656 fn hq_spacial_mode_uses_radius_zero_table_values() {
1657 for channels in [ChannelMode::Luma, ChannelMode::Chroma, ChannelMode::Yuv] {
1658 let opts = DenoiserOptions::builder()
1659 .channel_mode(channels)
1660 .mode(DenoisingMode::Spacial)
1661 .algorithm(hq(HqParams::default()))
1662 .build();
1663 let params = opts.to_nlm_params();
1664
1665 let expected = hq_default_strength(channels, 0);
1666 assert!(
1667 (params.strength - expected).abs() < f32::EPSILON,
1668 "for channels {channels:?} expected {expected}, got {}",
1669 params.strength
1670 );
1671 }
1672 }
1673
1674 #[test]
1675 fn hq_explicit_strength_wins_over_the_table_for_every_plane() {
1676 for channels in [ChannelMode::Luma, ChannelMode::Chroma, ChannelMode::Yuv] {
1677 let opts = DenoiserOptions::builder()
1678 .channel_mode(channels)
1679 .mode(DenoisingMode::Temporal { radius: 4 })
1680 .algorithm(Algorithm::NlmeansHq(NlmeansHqOptions {
1681 nlm: NlmeansOptions {
1682 tuning: NlmTuning {
1683 strength: Some(0.99),
1684 ..NlmTuning::default()
1685 },
1686 ..NlmeansOptions::default()
1687 },
1688 hq: HqParams::default(),
1689 }))
1690 .build();
1691 let params = opts.to_nlm_params();
1692
1693 assert!(
1694 (params.strength - 0.99).abs() < f32::EPSILON,
1695 "for channels {channels:?} the explicit strength was overridden by the table"
1696 );
1697 }
1698 }
1699
1700 #[test]
1701 fn fast_unset_strength_defaults_to_legacy_default() {
1702 let opts = DenoiserOptions::builder()
1703 .algorithm(Algorithm::Nlmeans(NlmeansOptions::default()))
1704 .build();
1705 let params = opts.to_nlm_params();
1706
1707 assert!((params.strength - 1.2).abs() < f32::EPSILON);
1708 }
1709
1710 #[test]
1711 fn nl4d_options_default_matches_nl4d_params_default() {
1712 let opts = Nl4dOptions::default();
1713 let params = crate::nl4d::Nl4dParams::default();
1714
1715 assert_eq!(opts.refine, params.refine);
1716 assert_eq!(opts.spatial_radius, params.spatial_radius);
1717 assert!((opts.c_min - params.c_min).abs() < f32::EPSILON);
1718 assert_eq!(opts.confidence_variance, params.confidence_variance);
1719 assert_eq!(opts.lambda_ht, None);
1726 assert!((params.lambda_ht - nl4d_default_lambda_ht(ChannelMode::Yuv)).abs() < f32::EPSILON);
1727 }
1728
1729 #[test]
1733 fn nl4d_builds_the_front_ends_hq_params_from_its_own_fields() {
1734 let opts = DenoiserOptions::builder()
1735 .mode(DenoisingMode::Temporal { radius: 2 })
1736 .algorithm(Algorithm::Nl4d(Nl4dOptions {
1737 sigma: Some(0.02),
1738 sigma_scale: 1.3,
1739 thsad_scale: 0.8,
1740 ..Nl4dOptions::default()
1741 }))
1742 .build();
1743 let params = opts.to_nlm_params();
1744
1745 let hq = params.hq.expect("nl4d always runs the hq front end");
1746 assert_eq!(hq.sigma_override, Some(0.02));
1747 assert!((hq.sigma_scale - 1.3).abs() < f32::EPSILON);
1748 assert!((hq.thsad_scale - 0.8).abs() < f32::EPSILON);
1749 assert!(
1750 hq.temporal_confidence,
1751 "the grouping kernel reads the confidence scores, so this cannot be off"
1752 );
1753 }
1754
1755 #[test]
1758 fn nl4d_reads_its_temporal_radius_from_the_denoising_mode() {
1759 for radius in [1u32, 4, 8] {
1760 let opts = DenoiserOptions::builder()
1761 .mode(DenoisingMode::Temporal { radius })
1762 .algorithm(Algorithm::Nl4d(Nl4dOptions::default()))
1763 .build();
1764
1765 assert_eq!(opts.to_nlm_params().temporal_radius, radius);
1766 }
1767 }
1768
1769 #[test]
1772 fn nl4d_never_builds_a_prefilter() {
1773 let opts = DenoiserOptions::builder()
1774 .mode(DenoisingMode::Temporal { radius: 2 })
1775 .algorithm(Algorithm::Nl4d(Nl4dOptions::default()))
1776 .build();
1777
1778 assert!(matches!(opts.to_nlm_params().prefilter, PrefilterMode::None));
1779 }
1780
1781 #[test]
1784 fn nl4d_leaves_the_nlm_weighting_knobs_at_their_defaults() {
1785 let defaults = NlmParams::default();
1786 let opts = DenoiserOptions::builder()
1787 .channel_mode(ChannelMode::Luma)
1788 .mode(DenoisingMode::Temporal { radius: 4 })
1789 .algorithm(Algorithm::Nl4d(Nl4dOptions::default()))
1790 .build();
1791 let params = opts.to_nlm_params();
1792
1793 assert!((params.strength - defaults.strength).abs() < f32::EPSILON);
1794 assert_eq!(params.search_radius, defaults.search_radius);
1795 assert_eq!(params.patch_radius, defaults.patch_radius);
1796 assert!((params.self_weight - defaults.self_weight).abs() < f32::EPSILON);
1797 }
1798
1799 #[test]
1802 fn nl4d_motion_search_becomes_an_active_mvtools_mode() {
1803 let opts = DenoiserOptions::builder()
1804 .mode(DenoisingMode::Temporal { radius: 2 })
1805 .algorithm(Algorithm::Nl4d(Nl4dOptions {
1806 motion: MotionSearch {
1807 blksize: 32,
1808 overlap: 16,
1809 search_radius: 6,
1810 pyramid_levels: 1,
1811 estimation: MotionEstimation::Direct,
1812 },
1813 ..Nl4dOptions::default()
1814 }))
1815 .build();
1816 let params = opts.to_nlm_params();
1817
1818 assert!(matches!(
1819 params.motion_compensation,
1820 MotionCompensationMode::Mvtools {
1821 blksize: 32,
1822 overlap: 16,
1823 search_radius: 6,
1824 pyramid_levels: 1,
1825 estimation: MotionEstimation::Direct,
1826 }
1827 ));
1828 }
1829
1830 #[test]
1831 fn nl4d_motion_search_defaults_match_the_front_ends_own_defaults() {
1832 let opts = DenoiserOptions::builder()
1833 .mode(DenoisingMode::Temporal { radius: 2 })
1834 .algorithm(Algorithm::Nl4d(Nl4dOptions::default()))
1835 .build();
1836 let params = opts.to_nlm_params();
1837
1838 assert_eq!(
1839 params.motion_compensation,
1840 crate::nl4d::Nl4dParams::default().nlm.motion_compensation
1841 );
1842 }
1843
1844 #[test]
1845 fn motion_compensation_passthrough() {
1846 let opts = DenoiserOptions::builder()
1847 .mode(DenoisingMode::Temporal { radius: 1 })
1848 .algorithm(Algorithm::Nlmeans(NlmeansOptions {
1849 motion_compensation: MotionCompensationMode::Mvtools {
1850 blksize: 16,
1851 overlap: 8,
1852 search_radius: 4,
1853 pyramid_levels: 2,
1854 estimation: MotionEstimation::Direct,
1855 },
1856 ..NlmeansOptions::default()
1857 }))
1858 .build();
1859 let params = opts.to_nlm_params();
1860
1861 assert!(matches!(
1862 params.motion_compensation,
1863 MotionCompensationMode::Mvtools {
1864 blksize: 16,
1865 overlap: 8,
1866 search_radius: 4,
1867 pyramid_levels: 2,
1868 ..
1869 }
1870 ));
1871 }
1872
1873 #[test]
1874 fn motion_compensation_defaults_to_none() {
1875 let opts = DenoiserOptions::builder().build();
1876 let params = opts.to_nlm_params();
1877 assert!(matches!(params.motion_compensation, MotionCompensationMode::None));
1878 }
1879
1880 #[test]
1881 fn nlm_tuning_overrides_individual_fields() {
1882 let defaults = NlmParams::default();
1883 let opts = DenoiserOptions::builder()
1884 .algorithm(fast_tuned(NlmTuning {
1885 search_radius: Some(7),
1886 patch_radius: None,
1887 strength: Some(2.5),
1888 self_weight: None,
1889 }))
1890 .build();
1891 let params = opts.to_nlm_params();
1892
1893 assert_eq!(params.search_radius, 7);
1894 assert_eq!(params.patch_radius, defaults.patch_radius);
1895 assert!((params.strength - 2.5).abs() < f32::EPSILON);
1896 assert!((params.self_weight - defaults.self_weight).abs() < f32::EPSILON);
1897 }
1898}
1899
1900#[cfg(all(test, feature = "vulkan"))]
1901mod tests {
1902 use super::*;
1903
1904 fn opts(mode: DenoisingMode) -> DenoiserOptions {
1905 DenoiserOptions::builder()
1906 .channel_mode(ChannelMode::Luma)
1907 .mode(mode)
1908 .build()
1909 }
1910
1911 fn frame(w: u32, h: u32) -> Vec<f32> {
1912 vec![0.5f32; (w * h) as usize]
1913 }
1914
1915 fn f32_out(out: FrameOutput) -> Vec<f32> {
1916 out.into_f32().expect("f32 output")
1917 }
1918
1919 #[test]
1920 fn spatial_denoise_roundtrip() {
1921 let mut d = Denoiser::create(
1922 &[Accelerator::Vulkan],
1923 &Device::Default,
1924 16,
1925 16,
1926 opts(DenoisingMode::Spacial),
1927 )
1928 .expect("denoiser construction failed");
1929 assert_eq!(d.selected_accelerator(), Accelerator::Vulkan);
1930
1931 d.push_frame(&frame(16, 16)).expect("push failed");
1932 let out = f32_out(d.recv_frame().expect("recv failed").expect("no frame"));
1933 assert_eq!(out.len(), 16 * 16);
1934 }
1935
1936 #[test]
1937 fn nl4d_algorithm_round_trips_through_the_facade() {
1938 let opts = DenoiserOptions::builder()
1939 .channel_mode(ChannelMode::Luma)
1940 .mode(DenoisingMode::Temporal { radius: 2 })
1941 .algorithm(Algorithm::Nl4d(Nl4dOptions::default()))
1942 .build();
1943 let mut d = Denoiser::create(&[Accelerator::Vulkan], &Device::Default, 16, 16, opts)
1944 .expect("nl4d denoiser construction failed");
1945 assert_eq!(d.selected_accelerator(), Accelerator::Vulkan);
1946
1947 d.push_frame(&frame(16, 16)).expect("push failed");
1951 assert!(d.recv_frame().expect("recv failed").is_none());
1952
1953 let mut out = Vec::new();
1954 d.flush(|f| out.push(f32_out(f))).expect("flush failed");
1955 assert_eq!(out.len(), 1, "expected exactly one output for one pushed frame");
1956 assert_eq!(out[0].len(), 16 * 16);
1957 }
1958
1959 #[test]
1963 fn nl4d_rejects_a_spatial_denoising_mode() {
1964 let opts = DenoiserOptions::builder()
1965 .channel_mode(ChannelMode::Luma)
1966 .mode(DenoisingMode::Spacial)
1967 .algorithm(Algorithm::Nl4d(Nl4dOptions::default()))
1968 .build();
1969 let result = Denoiser::create(&[Accelerator::Vulkan], &Device::Default, 16, 16, opts);
1970
1971 match result {
1972 Err(DenoiserError::Other(e)) => assert!(
1973 e.to_string().contains("temporal window"),
1974 "unexpected error message: {e}"
1975 ),
1976 Err(other) => panic!("expected DenoiserError::Other, got {other:?}"),
1977 Ok(_) => panic!("expected a rejection, got Ok"),
1978 }
1979 }
1980
1981 #[test]
1984 fn window_span_is_symmetric_for_nlmeans() {
1985 let opts = DenoiserOptions::builder()
1986 .channel_mode(ChannelMode::Luma)
1987 .mode(DenoisingMode::Temporal { radius: 3 })
1988 .algorithm(Algorithm::Nlmeans(NlmeansOptions::default()))
1989 .build();
1990 let d = Denoiser::create(&[Accelerator::Vulkan], &Device::Default, 16, 16, opts)
1991 .expect("denoiser construction failed");
1992
1993 let span = d.window_span();
1994 assert_eq!(span.behind, 3, "behind should equal the temporal radius");
1995 assert_eq!(span.ahead, 3, "ahead should equal the temporal radius");
1996 }
1997
1998 #[test]
2002 fn window_span_is_doubled_on_both_sides_for_nl4d() {
2003 let opts = DenoiserOptions::builder()
2004 .channel_mode(ChannelMode::Luma)
2005 .mode(DenoisingMode::Temporal { radius: 3 })
2006 .algorithm(Algorithm::Nl4d(Nl4dOptions::default()))
2007 .build();
2008 let d = Denoiser::create(&[Accelerator::Vulkan], &Device::Default, 16, 16, opts)
2009 .expect("nl4d denoiser construction failed");
2010
2011 let span = d.window_span();
2012 assert_eq!(span.behind, 6, "behind should equal 2 * the temporal radius");
2013 assert_eq!(span.ahead, 6, "ahead should equal 2 * the temporal radius");
2014 }
2015
2016 #[test]
2017 fn invalid_params_surface_as_error() {
2018 let bad = DenoiserOptions::builder()
2019 .algorithm(Algorithm::Nlmeans(NlmeansOptions {
2020 tuning: NlmTuning {
2021 strength: Some(0.0),
2022 ..NlmTuning::default()
2023 },
2024 ..NlmeansOptions::default()
2025 }))
2026 .build();
2027 let result = Denoiser::create(&[Accelerator::Vulkan], &Device::Default, 16, 16, bad);
2028
2029 match result {
2030 Err(DenoiserError::Other(_)) => {},
2031 Err(other) => panic!("expected DenoiserError::Other, got {other:?}"),
2032 Ok(_) => panic!("expected validation error, got Ok"),
2033 }
2034 }
2035
2036 #[test]
2037 fn tiny_frame_dimensions_surface_as_error() {
2038 let result = Denoiser::create(
2039 &[Accelerator::Vulkan],
2040 &Device::Default,
2041 2,
2042 2,
2043 opts(DenoisingMode::Spacial),
2044 );
2045
2046 match result {
2047 Err(DenoiserError::Other(e)) => {
2048 assert!(
2049 e.to_string().contains("supported minimum"),
2050 "unexpected error message: {e}"
2051 );
2052 },
2053 Err(other) => panic!("expected DenoiserError::Other, got {other:?}"),
2054 Ok(_) => panic!("expected dimension validation error, got Ok"),
2055 }
2056 }
2057
2058 #[test]
2059 fn push_after_pending_returns_queue_full() {
2060 let mut d = Denoiser::create(
2061 &[Accelerator::Vulkan],
2062 &Device::Default,
2063 16,
2064 16,
2065 opts(DenoisingMode::Spacial),
2066 )
2067 .unwrap();
2068
2069 d.push_frame(&frame(16, 16)).unwrap();
2074 d.push_frame(&frame(16, 16)).unwrap();
2075 let err = d.push_frame(&frame(16, 16)).expect_err("expected QueueFull");
2076 assert!(matches!(err, DenoiserError::QueueFull));
2077
2078 let out = f32_out(d.recv_frame().unwrap().unwrap());
2079 assert_eq!(out.len(), 16 * 16);
2080
2081 d.push_frame(&frame(16, 16)).expect("push after drain failed");
2083 }
2084
2085 #[test]
2088 fn queue_full_does_not_poison() {
2089 let mut d = Denoiser::create(
2090 &[Accelerator::Vulkan],
2091 &Device::Default,
2092 16,
2093 16,
2094 opts(DenoisingMode::Spacial),
2095 )
2096 .unwrap();
2097
2098 d.push_frame(&frame(16, 16)).unwrap();
2099 d.push_frame(&frame(16, 16)).unwrap();
2100 let err = d.push_frame(&frame(16, 16)).expect_err("expected QueueFull");
2101 assert!(matches!(err, DenoiserError::QueueFull));
2102 assert!(!d.poisoned, "QueueFull must not poison the denoiser");
2103
2104 d.recv_frame().unwrap().expect("recv failed after QueueFull");
2105
2106 d.push_frame(&frame(16, 16))
2109 .expect("push after QueueFull drain should succeed, not poison");
2110 }
2111
2112 #[test]
2113 fn poisoned_denoiser_refuses_every_entry_point() {
2114 let mut d = Denoiser::create(
2115 &[Accelerator::Vulkan],
2116 &Device::Default,
2117 16,
2118 16,
2119 opts(DenoisingMode::Spacial),
2120 )
2121 .unwrap();
2122 d.poisoned = true;
2123
2124 assert!(matches!(
2125 d.push_frame(&frame(16, 16)),
2126 Err(DenoiserError::Poisoned)
2127 ));
2128 assert!(matches!(d.recv_frame(), Err(DenoiserError::Poisoned)));
2129 assert!(matches!(d.try_recv_frame(), Err(DenoiserError::Poisoned)));
2130 assert!(matches!(d.flush(|_| {}), Err(DenoiserError::Poisoned)));
2131 }
2132
2133 #[test]
2134 fn reset_stream_clears_poison() {
2135 let mut d = Denoiser::create(
2136 &[Accelerator::Vulkan],
2137 &Device::Default,
2138 16,
2139 16,
2140 opts(DenoisingMode::Spacial),
2141 )
2142 .unwrap();
2143 d.poisoned = true;
2144
2145 d.reset_stream();
2146 assert!(!d.poisoned, "reset_stream must clear the poison flag");
2147
2148 d.push_frame(&frame(16, 16))
2149 .expect("push after reset_stream should succeed");
2150 }
2151
2152 fn frame_filled(w: u32, h: u32, value: f32) -> Vec<f32> {
2153 vec![value; (w * h) as usize]
2154 }
2155
2156 fn push_n_with_drain(d: &mut Denoiser, n: usize, value: f32, out: &mut Vec<Vec<f32>>) {
2159 for _ in 0..n {
2160 loop {
2161 match d.push_frame(&frame_filled(16, 16, value)) {
2162 Ok(()) => break,
2163 Err(DenoiserError::QueueFull) => {
2164 let f = d
2165 .recv_frame()
2166 .expect("recv ok")
2167 .expect("queue full but recv yielded none");
2168 out.push(f32_out(f));
2169 },
2170 Err(e) => panic!("unexpected push error: {e:?}"),
2171 }
2172 }
2173 }
2174 }
2175
2176 #[test]
2177 fn flush_leaves_denoiser_reusable_spatial() {
2178 let mut d = Denoiser::create(
2179 &[Accelerator::Vulkan],
2180 &Device::Default,
2181 16,
2182 16,
2183 opts(DenoisingMode::Spacial),
2184 )
2185 .unwrap();
2186
2187 let mut batch_a = Vec::new();
2188 push_n_with_drain(&mut d, 5, 0.25, &mut batch_a);
2189 d.flush(|f| batch_a.push(f32_out(f))).expect("first flush failed");
2190 assert_eq!(batch_a.len(), 5);
2191
2192 assert!(d.recv_frame().unwrap().is_none());
2194
2195 let mut batch_b = Vec::new();
2196 push_n_with_drain(&mut d, 5, 0.75, &mut batch_b);
2197 d.flush(|f| batch_b.push(f32_out(f)))
2198 .expect("second flush failed");
2199 assert_eq!(batch_b.len(), 5);
2200
2201 for v in batch_b.iter().flatten() {
2202 assert!((v - 0.75).abs() < 0.1, "batch_b carried state from batch_a: {v}");
2203 }
2204 for v in batch_a.iter().flatten() {
2205 assert!((v - 0.25).abs() < 0.1, "batch_a value unexpectedly drifted: {v}");
2206 }
2207 }
2208
2209 #[test]
2210 fn flush_leaves_denoiser_reusable_temporal() {
2211 let mut d = Denoiser::create(
2212 &[Accelerator::Vulkan],
2213 &Device::Default,
2214 16,
2215 16,
2216 opts(DenoisingMode::Temporal { radius: 1 }),
2217 )
2218 .unwrap();
2219
2220 let mut batch_a = Vec::new();
2221 push_n_with_drain(&mut d, 5, 0.25, &mut batch_a);
2222 d.flush(|f| batch_a.push(f32_out(f))).expect("first flush failed");
2223 assert_eq!(batch_a.len(), 5, "expected 5 frames from first batch");
2224
2225 assert!(d.recv_frame().unwrap().is_none());
2230 d.push_frame(&frame_filled(16, 16, 0.75)).unwrap();
2231 assert!(
2232 d.recv_frame().unwrap().is_none(),
2233 "first push of new temporal stream should not produce output yet"
2234 );
2235
2236 let mut batch_b = Vec::new();
2238 push_n_with_drain(&mut d, 4, 0.75, &mut batch_b);
2239 d.flush(|f| batch_b.push(f32_out(f)))
2240 .expect("second flush failed");
2241 assert_eq!(batch_b.len(), 5, "expected 5 frames from second batch");
2242
2243 for v in batch_b.iter().flatten() {
2244 assert!((v - 0.75).abs() < 0.1, "batch_b carried state from batch_a: {v}");
2245 }
2246 }
2247
2248 #[test]
2249 fn flush_emits_exactly_n_outputs_for_small_n() {
2250 for n in 1..=5usize {
2255 let mut d = Denoiser::create(
2256 &[Accelerator::Vulkan],
2257 &Device::Default,
2258 16,
2259 16,
2260 opts(DenoisingMode::Temporal { radius: 2 }),
2261 )
2262 .unwrap();
2263
2264 let mut out = Vec::new();
2265 push_n_with_drain(&mut d, n, 0.5, &mut out);
2266 d.flush(|f| out.push(f32_out(f))).expect("flush failed");
2267 assert_eq!(
2268 out.len(),
2269 n,
2270 "expected {n} outputs for {n} pushes, got {}",
2271 out.len()
2272 );
2273 }
2274 }
2275
2276 #[test]
2283 fn dropping_a_polled_pending_frame_does_not_poison_the_device() {
2284 let new = || {
2285 Denoiser::create(
2286 &[Accelerator::Vulkan],
2287 &Device::Default,
2288 64,
2289 64,
2290 opts(DenoisingMode::Spacial),
2291 )
2292 .unwrap()
2293 };
2294
2295 let mut d = new();
2296 d.push_frame(&frame(64, 64)).unwrap();
2297 let _ = d.try_recv_frame().unwrap();
2300 drop(d);
2301
2302 let mut d = new();
2305 for _ in 0..4 {
2306 d.push_frame(&frame(64, 64)).unwrap();
2307 d.recv_frame()
2308 .expect("readback after a dropped polled frame should not fail")
2309 .expect("spatial mode emits one frame per push");
2310 }
2311 d.flush(|_| {}).unwrap();
2312 }
2313}