use super::{MotionCompensationMode, PrefilterMode, prefilter};
pub(super) const NLM_NORM: f32 = 255.0 * 255.0;
pub(super) const NLM_LEGACY: f32 = 3.0;
const HQ_DEFAULT_STRENGTH_LUMA: [f32; 9] = [0.45, 0.45, 0.42, 0.42, 0.35, 0.35, 0.35, 0.30, 0.30];
const HQ_DEFAULT_STRENGTH_CHROMA: [f32; 9] = [1.00, 0.85, 0.70, 0.70, 0.70, 0.70, 0.70, 0.70, 0.70];
pub fn hq_default_strength(channels: ChannelMode, temporal_radius: u32) -> f32 {
let idx = temporal_radius.min(MAX_TEMPORAL_RADIUS) as usize;
match channels {
ChannelMode::Luma | ChannelMode::Yuv => HQ_DEFAULT_STRENGTH_LUMA[idx],
ChannelMode::Chroma => HQ_DEFAULT_STRENGTH_CHROMA[idx],
}
}
pub const MIN_FRAME_DIM: u32 = 3;
pub fn validate_dimensions(width: u32, height: u32) -> Result<(), anyhow::Error> {
if width < MIN_FRAME_DIM || height < MIN_FRAME_DIM {
anyhow::bail!(
"frame dimensions {width}x{height} are below the supported minimum \
({MIN_FRAME_DIM}x{MIN_FRAME_DIM}); the noise estimate needs at least \
one interior pixel"
);
}
Ok(())
}
pub(super) const SEPARABLE_THRESHOLD: u32 = 8;
pub const MAX_PATCH_RADIUS: u32 = 16;
pub const MAX_SEARCH_RADIUS: u32 = 8;
pub const MAX_TEMPORAL_RADIUS: u32 = 8;
pub const MAX_BILATERAL_RADIUS: u32 = 22;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelMode {
Luma,
Chroma,
Yuv,
}
impl ChannelMode {
pub fn count(self) -> u32 {
match self {
ChannelMode::Luma => 1,
ChannelMode::Chroma => 2,
ChannelMode::Yuv => 3,
}
}
pub fn storage_count(self) -> u32 {
match self {
ChannelMode::Luma => 1,
ChannelMode::Chroma => 2,
ChannelMode::Yuv => 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HqParams {
pub auto_strength: bool,
pub noise_floor: bool,
pub sigma_override: Option<f32>,
pub temporal_confidence: bool,
pub thsad_scale: f32,
pub sigma_scale: f32,
}
impl Default for HqParams {
fn default() -> Self {
Self {
auto_strength: true,
noise_floor: true,
sigma_override: None,
temporal_confidence: true,
thsad_scale: 1.0,
sigma_scale: 1.0,
}
}
}
impl HqParams {
pub fn with_sigma(sigma: f32) -> Self {
Self {
sigma_override: Some(sigma),
..Self::default()
}
}
}
#[derive(Debug, Clone)]
pub struct NlmParams {
pub temporal_radius: u32,
pub search_radius: u32,
pub patch_radius: u32,
pub strength: f32,
pub self_weight: f32,
pub channels: ChannelMode,
pub prefilter: PrefilterMode,
pub motion_compensation: MotionCompensationMode,
pub hq: Option<HqParams>,
}
impl Default for NlmParams {
fn default() -> Self {
Self {
temporal_radius: 0,
search_radius: 2,
patch_radius: 4,
strength: 1.2,
self_weight: 1.0,
channels: ChannelMode::Yuv,
prefilter: PrefilterMode::None,
motion_compensation: MotionCompensationMode::None,
hq: None,
}
}
}
impl NlmParams {
pub(super) fn effective_strength_with(&self, sigma_eff: Option<f32>) -> f32 {
match (self.hq, sigma_eff) {
(Some(hq), Some(sigma)) if hq.auto_strength => self.strength * sigma * 255.0,
_ => self.strength,
}
}
pub fn h2_inv_norm_with(&self, sigma_eff: Option<f32>) -> f32 {
let s_size = (2 * self.patch_radius + 1) * (2 * self.patch_radius + 1);
let s = self.effective_strength_with(sigma_eff);
NLM_NORM / (NLM_LEGACY * s * s * s_size as f32)
}
pub fn h2_inv_norm(&self) -> f32 {
self.h2_inv_norm_with(self.hq.and_then(|hq| hq.sigma_override))
}
pub(super) fn noise_offset_with(&self, sigmas: Option<&[f32]>) -> f32 {
match (self.hq, sigmas) {
(Some(hq), Some(sigmas)) if hq.noise_floor => {
let s_size = (2 * self.patch_radius + 1) * (2 * self.patch_radius + 1);
let scale = channel_scale(self.channels);
let count = self.channels.count() as usize;
let sum_sq: f32 = sigmas.iter().take(count).map(|&s| s * s).sum();
2.0 * scale * sum_sq * s_size as f32
},
_ => 0.0,
}
}
pub(super) fn noise_offset(&self) -> f32 {
match self.hq.and_then(|hq| hq.sigma_override) {
Some(sigma) => {
let sigmas = [sigma; 3];
self.noise_offset_with(Some(&sigmas[..self.channels.count() as usize]))
},
None => 0.0,
}
}
pub(super) fn total_frames(&self) -> u32 {
1 + 2 * self.temporal_radius
}
pub fn validate(&self) -> Result<(), anyhow::Error> {
if self.patch_radius > MAX_PATCH_RADIUS {
anyhow::bail!(
"patch_radius={} exceeds the supported maximum ({}); larger patches \
exhaust on-chip SMEM in the fused/windowed kernels",
self.patch_radius,
MAX_PATCH_RADIUS,
);
}
if self.search_radius > MAX_SEARCH_RADIUS {
anyhow::bail!(
"search_radius={} exceeds the supported maximum ({}). The windowed \
kernel's search window loop is fully unrolled, so its compiled size \
and codegen time both grow with search_radius",
self.search_radius,
MAX_SEARCH_RADIUS,
);
}
if self.temporal_radius > MAX_TEMPORAL_RADIUS {
anyhow::bail!(
"temporal_radius={} exceeds the supported maximum ({}); the ring \
buffer grows linearly with the window size",
self.temporal_radius,
MAX_TEMPORAL_RADIUS,
);
}
if !(self.strength.is_finite() && self.strength > 0.0) {
anyhow::bail!(
"strength must be finite and > 0 (got {}); strength = 0 produces an \
infinite Welsch normalisation factor",
self.strength,
);
}
if !self.self_weight.is_finite() || self.self_weight < 0.0 {
anyhow::bail!("self_weight must be finite and >= 0 (got {})", self.self_weight,);
}
if let Some(hq) = self.hq
&& let Some(sigma) = hq.sigma_override
&& (!sigma.is_finite() || sigma <= 0.0 || sigma > 1.0)
{
anyhow::bail!(
"hq sigma_override must be finite and in (0, 1] in normalised units (got {})",
sigma,
);
}
if let Some(hq) = self.hq
&& !(hq.thsad_scale.is_finite() && hq.thsad_scale > 0.0)
{
anyhow::bail!(
"hq thsad_scale must be finite and > 0 (got {}); thsad_scale = 0 collapses \
every block's confidence to zero regardless of match quality",
hq.thsad_scale,
);
}
if let Some(hq) = self.hq
&& !(hq.sigma_scale.is_finite() && (0.1..=10.0).contains(&hq.sigma_scale))
{
anyhow::bail!(
"hq sigma_scale must be finite and in [0.1, 10.0] (got {})",
hq.sigma_scale,
);
}
if let PrefilterMode::Bilateral { sigma_s, sigma_r } = self.prefilter {
if !sigma_s.is_finite() || sigma_s <= 0.0 {
anyhow::bail!(
"bilateral prefilter sigma_s must be finite and > 0 (got {}); \
sigma_s = 0 produces an infinite spatial-weight normalisation factor",
sigma_s,
);
}
if !sigma_r.is_finite() || sigma_r <= 0.0 {
anyhow::bail!(
"bilateral prefilter sigma_r must be finite and > 0 (got {}); \
sigma_r = 0 produces an infinite range-weight normalisation factor \
that turns the centre tap into NaN",
sigma_r,
);
}
let bilateral_radius = prefilter::bilateral_radius(sigma_s);
if bilateral_radius > MAX_BILATERAL_RADIUS {
anyhow::bail!(
"bilateral prefilter sigma_s={} implies a shared-memory tile radius of {} \
(radius = ceil(2 * sigma_s), minimum 1), exceeding the supported maximum \
({}); larger radii exhaust on-chip SMEM in the bilateral kernel",
sigma_s,
bilateral_radius,
MAX_BILATERAL_RADIUS,
);
}
if !prefilter::inv_two_sigma_sq(sigma_s).is_finite() {
anyhow::bail!(
"bilateral prefilter sigma_s is too small (got {}); sigma_s * sigma_s \
underflows to 0 in f32, making the spatial-weight normalisation factor \
infinite",
sigma_s,
);
}
if !prefilter::inv_two_sigma_sq(sigma_r).is_finite() {
anyhow::bail!(
"bilateral prefilter sigma_r is too small (got {}); sigma_r * sigma_r \
underflows to 0 in f32, making the range-weight normalisation factor \
infinite and the centre tap NaN",
sigma_r,
);
}
}
if let PrefilterMode::NlmSpatial { strength_scale } = self.prefilter {
if !strength_scale.is_finite() || strength_scale <= 0.0 {
anyhow::bail!(
"nlm pilot strength_scale must be finite and > 0 (got {})",
strength_scale,
);
}
if self.patch_radius > SEPARABLE_THRESHOLD {
anyhow::bail!(
"the nlm pilot uses the windowed spatial kernel, which supports \
patch_radius up to {} (got {})",
SEPARABLE_THRESHOLD,
self.patch_radius,
);
}
}
self.motion_compensation.validate()?;
Ok(())
}
}
pub(super) fn channel_scale(channels: ChannelMode) -> f32 {
match channels {
ChannelMode::Luma => 3.0,
ChannelMode::Chroma => 1.5,
ChannelMode::Yuv => 1.0,
}
}
pub(super) fn sigma_eff(sigmas: &[f32], channels: ChannelMode) -> f32 {
let count = channels.count() as usize;
let sum_sq: f32 = sigmas.iter().take(count).map(|&s| s * s).sum();
(sum_sq / count as f32).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noise_offset_scales_with_sigma_and_patch_size() {
let sigma = 4.0 / 255.0;
let params = NlmParams {
patch_radius: 4,
hq: Some(HqParams::with_sigma(sigma)),
..NlmParams::default()
};
let expected = 6.0 * sigma * sigma * 81.0;
assert!(
(params.noise_offset() - expected).abs() < 1e-6,
"expected {expected}, got {}",
params.noise_offset()
);
}
#[test]
fn noise_offset_zero_without_noise_floor() {
let params = NlmParams {
hq: Some(HqParams {
auto_strength: true,
noise_floor: false,
sigma_override: Some(4.0 / 255.0),
temporal_confidence: true,
thsad_scale: 1.0,
sigma_scale: 1.0,
}),
..NlmParams::default()
};
assert_eq!(params.noise_offset(), 0.0);
}
#[test]
fn noise_offset_zero_without_hq() {
let params = NlmParams::default();
assert_eq!(params.noise_offset(), 0.0);
}
#[test]
fn h2_inv_norm_with_auto_strength_matches_hand_computed() {
let sigma = 8.0 / 255.0;
let params = NlmParams {
strength: 1.0,
hq: Some(HqParams::with_sigma(sigma)),
..NlmParams::default()
};
let s_size = (2 * params.patch_radius + 1) * (2 * params.patch_radius + 1);
let effective_strength = 1.0 * sigma * 255.0;
let expected = NLM_NORM / (NLM_LEGACY * effective_strength * effective_strength * s_size as f32);
assert!(
(params.h2_inv_norm() - expected).abs() < 1e-6,
"expected {expected}, got {}",
params.h2_inv_norm()
);
}
#[test]
fn validate_rejects_zero_hq_sigma() {
let params = NlmParams {
hq: Some(HqParams::with_sigma(0.0)),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_rejects_hq_sigma_above_one() {
let params = NlmParams {
hq: Some(HqParams::with_sigma(1.5)),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_rejects_nan_hq_sigma() {
let params = NlmParams {
hq: Some(HqParams::with_sigma(f32::NAN)),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_rejects_zero_thsad_scale() {
let params = NlmParams {
hq: Some(HqParams {
thsad_scale: 0.0,
..HqParams::default()
}),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_rejects_negative_thsad_scale() {
let params = NlmParams {
hq: Some(HqParams {
thsad_scale: -1.0,
..HqParams::default()
}),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_rejects_nan_thsad_scale() {
let params = NlmParams {
hq: Some(HqParams {
thsad_scale: f32::NAN,
..HqParams::default()
}),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_accepts_default_thsad_scale() {
let params = NlmParams {
hq: Some(HqParams::default()),
..NlmParams::default()
};
assert!(params.validate().is_ok());
}
#[test]
fn hq_params_default_sigma_scale_is_one() {
assert_eq!(HqParams::default().sigma_scale, 1.0);
}
#[test]
fn validate_rejects_sigma_scale_below_the_minimum() {
let params = NlmParams {
hq: Some(HqParams {
sigma_scale: 0.05,
..HqParams::default()
}),
..NlmParams::default()
};
let err = params.validate().expect_err("0.05 is below the 0.1 minimum");
assert!(
err.to_string().contains("hq sigma_scale"),
"error should name the field, got: {err}"
);
}
#[test]
fn validate_rejects_sigma_scale_above_the_maximum() {
let params = NlmParams {
hq: Some(HqParams {
sigma_scale: 10.5,
..HqParams::default()
}),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_rejects_nan_sigma_scale() {
let params = NlmParams {
hq: Some(HqParams {
sigma_scale: f32::NAN,
..HqParams::default()
}),
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_accepts_sigma_scale_at_the_bounds() {
let low = NlmParams {
hq: Some(HqParams {
sigma_scale: 0.1,
..HqParams::default()
}),
..NlmParams::default()
};
assert!(low.validate().is_ok());
let high = NlmParams {
hq: Some(HqParams {
sigma_scale: 10.0,
..HqParams::default()
}),
..NlmParams::default()
};
assert!(high.validate().is_ok());
}
#[test]
fn noise_offset_with_handles_distinct_per_channel_sigmas() {
let sigma_u = 4.0 / 255.0;
let sigma_v = 10.0 / 255.0;
let params = NlmParams {
patch_radius: 4,
channels: ChannelMode::Chroma,
hq: Some(HqParams {
auto_strength: true,
noise_floor: true,
sigma_override: None,
temporal_confidence: true,
thsad_scale: 1.0,
sigma_scale: 1.0,
}),
..NlmParams::default()
};
let s_size = (2 * params.patch_radius + 1) * (2 * params.patch_radius + 1);
let expected = 2.0 * 1.5 * (sigma_u * sigma_u + sigma_v * sigma_v) * s_size as f32;
let got = params.noise_offset_with(Some(&[sigma_u, sigma_v]));
assert!((got - expected).abs() < 1e-9, "expected {expected}, got {got}");
}
#[test]
fn sigma_eff_is_rms_over_active_channels() {
let sigmas = [3.0 / 255.0, 4.0 / 255.0];
let got = sigma_eff(&sigmas, ChannelMode::Chroma);
let expected = ((sigmas[0] * sigmas[0] + sigmas[1] * sigmas[1]) / 2.0).sqrt();
assert!((got - expected).abs() < 1e-9, "expected {expected}, got {got}");
}
#[test]
fn validate_rejects_non_positive_pilot_strength_scale() {
let zero = NlmParams {
prefilter: PrefilterMode::NlmSpatial { strength_scale: 0.0 },
..NlmParams::default()
};
assert!(zero.validate().is_err());
let nan = NlmParams {
prefilter: PrefilterMode::NlmSpatial {
strength_scale: f32::NAN,
},
..NlmParams::default()
};
assert!(nan.validate().is_err());
}
#[test]
fn validate_rejects_pilot_with_patch_radius_above_separable_threshold() {
let params = NlmParams {
prefilter: PrefilterMode::NlmSpatial { strength_scale: 1.0 },
patch_radius: SEPARABLE_THRESHOLD + 1,
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_accepts_pilot_within_limits() {
let params = NlmParams {
prefilter: PrefilterMode::NlmSpatial { strength_scale: 1.0 },
patch_radius: SEPARABLE_THRESHOLD,
..NlmParams::default()
};
assert!(params.validate().is_ok());
}
#[test]
fn validate_rejects_non_positive_bilateral_sigma_r() {
let params = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 3.0,
sigma_r: 0.0,
},
..NlmParams::default()
};
assert!(params.validate().is_err());
let negative = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 3.0,
sigma_r: -0.02,
},
..NlmParams::default()
};
assert!(negative.validate().is_err());
let nan = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 3.0,
sigma_r: f32::NAN,
},
..NlmParams::default()
};
assert!(nan.validate().is_err());
let inf = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 3.0,
sigma_r: f32::INFINITY,
},
..NlmParams::default()
};
assert!(inf.validate().is_err());
}
#[test]
fn validate_rejects_non_positive_bilateral_sigma_s() {
let params = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 0.0,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(params.validate().is_err());
let negative = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: -3.0,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(negative.validate().is_err());
let nan = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: f32::NAN,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(nan.validate().is_err());
let inf = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: f32::INFINITY,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(inf.validate().is_err());
}
#[test]
fn validate_accepts_positive_finite_bilateral_sigmas() {
let params = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 3.0,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(params.validate().is_ok());
}
#[test]
fn validate_accepts_a_small_positive_bilateral_sigma_at_the_boundary() {
let safe_small = 1e-6_f32;
assert!(
(safe_small * safe_small).is_normal(),
"test value itself must not underflow"
);
let small_sigma_s = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: safe_small,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(small_sigma_s.validate().is_ok());
let small_sigma_r = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 3.0,
sigma_r: safe_small,
},
..NlmParams::default()
};
assert!(small_sigma_r.validate().is_ok());
}
#[test]
fn validate_rejects_a_subnormal_bilateral_sigma_that_underflows_on_squaring() {
let sq = f32::MIN_POSITIVE * f32::MIN_POSITIVE;
assert_eq!(
sq, 0.0,
"test assumption: MIN_POSITIVE must underflow on squaring"
);
let inv = prefilter::inv_two_sigma_sq(f32::MIN_POSITIVE);
assert!(
!inv.is_finite(),
"test assumption: the derived factor must be infinite here"
);
let sigma_s = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: f32::MIN_POSITIVE,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(
sigma_s.validate().is_err(),
"a subnormal sigma_s that underflows to an infinite normalisation factor must be rejected"
);
let sigma_r = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 3.0,
sigma_r: f32::MIN_POSITIVE,
},
..NlmParams::default()
};
assert!(
sigma_r.validate().is_err(),
"a subnormal sigma_r that underflows to an infinite normalisation factor must be rejected"
);
}
#[test]
fn validate_rejects_bilateral_sigma_s_above_the_smem_ceiling() {
let params = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 16.0,
sigma_r: 0.02,
},
..NlmParams::default()
};
let err = params.validate().expect_err("radius 32 exceeds the 22 ceiling");
assert!(
err.to_string().contains("sigma_s"),
"error should name the field, got: {err}"
);
}
#[test]
fn validate_rejects_extreme_bilateral_sigma_s() {
let params = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 1e9,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn validate_accepts_bilateral_sigma_s_at_the_smem_ceiling() {
let params = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 11.0,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(params.validate().is_ok());
}
#[test]
fn validate_rejects_bilateral_sigma_s_just_above_the_smem_ceiling() {
let params = NlmParams {
prefilter: PrefilterMode::Bilateral {
sigma_s: 11.01,
sigma_r: 0.02,
},
..NlmParams::default()
};
assert!(params.validate().is_err());
}
#[test]
fn sigma_eff_ignores_channels_past_the_mode_count() {
let sigmas = [6.0 / 255.0, 100.0 / 255.0, 200.0 / 255.0];
let got = sigma_eff(&sigmas, ChannelMode::Luma);
assert!(
(got - sigmas[0]).abs() < 1e-9,
"expected {}, got {got}",
sigmas[0]
);
}
#[test]
fn hq_default_strength_matches_the_measured_luma_table() {
const EXPECTED: [f32; 9] = [0.45, 0.45, 0.42, 0.42, 0.35, 0.35, 0.35, 0.30, 0.30];
for (radius, &expected) in EXPECTED.iter().enumerate() {
let got = hq_default_strength(ChannelMode::Luma, radius as u32);
assert!(
(got - expected).abs() < f32::EPSILON,
"radius={radius}: expected {expected}, got {got}"
);
}
}
#[test]
fn hq_default_strength_matches_the_measured_chroma_table() {
const EXPECTED: [f32; 9] = [1.00, 0.85, 0.70, 0.70, 0.70, 0.70, 0.70, 0.70, 0.70];
for (radius, &expected) in EXPECTED.iter().enumerate() {
let got = hq_default_strength(ChannelMode::Chroma, radius as u32);
assert!(
(got - expected).abs() < f32::EPSILON,
"radius={radius}: expected {expected}, got {got}"
);
}
}
#[test]
fn hq_default_strength_yuv_reads_the_luma_table() {
for radius in 0..=8u32 {
let yuv = hq_default_strength(ChannelMode::Yuv, radius);
let luma = hq_default_strength(ChannelMode::Luma, radius);
assert!(
(yuv - luma).abs() < f32::EPSILON,
"radius={radius}: yuv={yuv}, luma={luma}"
);
}
}
#[test]
fn validate_dimensions_rejects_frames_below_the_minimum() {
assert!(validate_dimensions(2, 64).is_err());
assert!(validate_dimensions(64, 2).is_err());
assert!(validate_dimensions(0, 0).is_err());
}
#[test]
fn validate_dimensions_accepts_the_minimum() {
assert!(validate_dimensions(MIN_FRAME_DIM, MIN_FRAME_DIM).is_ok());
assert!(validate_dimensions(1920, 1080).is_ok());
}
#[test]
fn hq_default_strength_clamps_radius_above_the_table() {
let at_max = hq_default_strength(ChannelMode::Luma, MAX_TEMPORAL_RADIUS);
let above_max = hq_default_strength(ChannelMode::Luma, MAX_TEMPORAL_RADIUS + 5);
assert!(
(at_max - above_max).abs() < f32::EPSILON,
"expected clamping to hold the last table entry, got {at_max} vs {above_max}"
);
}
}