use glam::Vec3;
use indicatrix::renderer::{
denoise::{AtrousDenoiser, AtrousParams, GBuffers, taper_strength},
frame_denoise::{DenoiseScratch, FirstHitSnapshot, denoise_and_tonemap_frame},
tonemap::tonemap_to_rgba,
};
use std::ops::Range;
struct Xorshift32(u32);
impl Xorshift32 {
const fn new(seed: u32) -> Self {
Self(if seed == 0 { 0xdead_beef } else { seed })
}
const fn next_u32(&mut self) -> u32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
x
}
#[expect(
clippy::cast_possible_truncation,
reason = "the quotient is in [0, 1], well inside f32 range"
)]
fn next_f32(&mut self) -> f32 {
(f64::from(self.next_u32()) / f64::from(u32::MAX)) as f32
}
fn next_signed(&mut self) -> f32 {
self.next_f32().mul_add(2.0, -1.0)
}
}
fn mse(a: &[Vec3], b: &[Vec3]) -> f64 {
assert_eq!(a.len(), b.len());
let mut sum = 0.0f64;
for (x, y) in a.iter().zip(b.iter()) {
let d = *x - *y;
sum += f64::from(d.length_squared());
}
sum / a.len() as f64
}
fn all_finite(buf: &[Vec3]) -> bool {
buf.iter()
.all(|v| v.x.is_finite() && v.y.is_finite() && v.z.is_finite())
}
fn region_mse(buf: &[Vec3], truth: Vec3, width: usize, xs: &Range<usize>, ys: Range<usize>) -> f64 {
let mut sum = 0.0f64;
let mut count = 0usize;
for y in ys {
for x in xs.clone() {
sum += f64::from((buf[y * width + x] - truth).length_squared());
count += 1;
}
}
sum / count as f64
}
#[test]
fn filtering_reduces_error_against_ground_truth() {
let width = 48;
let height = 48;
let len = width * height;
let color_a = Vec3::new(0.6, 0.5, 0.2);
let color_b = Vec3::new(0.1, 0.3, 0.7);
let mut ground_truth = vec![Vec3::ZERO; len];
let mut facet_id = vec![0i32; len];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
if x < width / 2 {
ground_truth[idx] = color_a;
facet_id[idx] = 0;
} else {
ground_truth[idx] = color_b;
facet_id[idx] = 1;
}
}
}
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let mut rng = Xorshift32::new(12345);
let mut noisy = ground_truth.clone();
for c in &mut noisy {
let n = Vec3::new(rng.next_signed(), rng.next_signed(), rng.next_signed()) * 0.4;
*c = (*c + n).max(Vec3::ZERO);
}
let inputs = GBuffers {
color: &noisy,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
let mse_before = mse(&noisy, &ground_truth);
let mse_after = mse(&filtered, &ground_truth);
assert!(
mse_after < mse_before * 0.5,
"filtering should substantially reduce error against ground truth: mse_before={mse_before:.6}, mse_after={mse_after:.6}"
);
}
#[test]
fn facet_edges_survive_filtering() {
let width = 64;
let height = 32;
let len = width * height;
let split = width / 2;
let color_a = Vec3::new(0.9, 0.05, 0.05);
let color_b = Vec3::new(0.05, 0.05, 0.9);
let mut color = vec![Vec3::ZERO; len];
let mut facet_id = vec![0i32; len];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
if x < split {
color[idx] = color_a;
facet_id[idx] = 7;
} else {
color[idx] = color_b;
facet_id[idx] = 42;
}
}
}
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let params = AtrousParams {
num_passes: 5,
..AtrousParams::default()
};
let filtered = denoiser.denoise(&inputs, ¶ms);
let y = height / 2;
let left_idx = y * width + (split - 1);
let right_idx = y * width + split;
let original_jump = (color[right_idx] - color[left_idx]).length();
let filtered_jump = (filtered[right_idx] - filtered[left_idx]).length();
assert!(
filtered_jump > original_jump * 0.95,
"facet boundary must stay sharp: original_jump={original_jump:.4}, filtered_jump={filtered_jump:.4}"
);
let mut rng = Xorshift32::new(0xfeed);
let noisy: Vec<Vec3> = color
.iter()
.map(|c| *c + Vec3::new(rng.next_signed(), rng.next_signed(), rng.next_signed()) * 0.15)
.collect();
let noisy_inputs = GBuffers {
color: &noisy,
..inputs
};
let cleaned = denoiser.denoise(&noisy_inputs, ¶ms);
let rows = 4..height - 4;
for (truth, xs) in [(color_a, 4..split - 4), (color_b, split + 4..width - 4)] {
let before = region_mse(&noisy, truth, width, &xs, rows.clone());
let after = region_mse(&cleaned, truth, width, &xs, rows.clone());
assert!(
after * 4.0 <= before,
"a flat noisy region must lose at least 3/4 of its error: before={before:.6}, after={after:.6}"
);
}
}
#[test]
fn path_signature_boundary_survives_inside_one_facet() {
let width = 64;
let height = 32;
let len = width * height;
let split = width / 2;
let color_a = Vec3::new(0.30, 0.20, 0.10);
let color_b = Vec3::new(0.34, 0.22, 0.12);
let mut color = vec![Vec3::ZERO; len];
let mut sig = vec![0u32; len];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
if x < split {
color[idx] = color_a;
sig[idx] = 0x1111;
} else {
color[idx] = color_b;
sig[idx] = 0x2222;
}
}
}
let facet_id = vec![3i32; len];
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let without = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let with_sig = GBuffers {
path_sig: Some(&sig),
..without
};
let params = AtrousParams {
num_passes: 5,
..AtrousParams::default()
};
let mut denoiser = AtrousDenoiser::new();
let y = height / 2;
let left_idx = y * width + (split - 1);
let right_idx = y * width + split;
let original_jump = (color[right_idx] - color[left_idx]).length();
let smeared = denoiser.denoise(&without, ¶ms);
let smeared_jump = (smeared[right_idx] - smeared[left_idx]).length();
assert!(
smeared_jump < original_jump * 0.5,
"premise: colour alone must smear this boundary: original={original_jump:.4}, smeared={smeared_jump:.4}"
);
let kept = denoiser.denoise(&with_sig, ¶ms);
let kept_jump = (kept[right_idx] - kept[left_idx]).length();
assert!(
kept_jump > original_jump * 0.95,
"the signature boundary must stay sharp: original={original_jump:.4}, kept={kept_jump:.4}"
);
let mut rng = Xorshift32::new(0xbeef);
let noisy: Vec<Vec3> = color
.iter()
.map(|c| *c + Vec3::new(rng.next_signed(), rng.next_signed(), rng.next_signed()) * 0.1)
.collect();
let cleaned = denoiser.denoise(
&GBuffers {
color: &noisy,
..with_sig
},
¶ms,
);
let rows = 4..height - 4;
for (truth, xs) in [(color_a, 4..split - 4), (color_b, split + 4..width - 4)] {
let before = region_mse(&noisy, truth, width, &xs, rows.clone());
let after = region_mse(&cleaned, truth, width, &xs, rows.clone());
assert!(
after * 4.0 <= before,
"a noisy region must lose at least 3/4 of its error: before={before:.6}, after={after:.6}"
);
}
}
#[test]
fn high_sample_count_approaches_identity() {
let width = 16;
let height = 16;
let len = width * height;
let mut rng = Xorshift32::new(999);
let mut color = vec![Vec3::ZERO; len];
let mut facet_id = vec![0i32; len];
for (i, c) in color.iter_mut().enumerate() {
*c = Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32());
facet_id[i] = i32::try_from(i % 3).expect("a remainder below 3");
}
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 100_000,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
for (f, c) in filtered.iter().zip(color.iter()) {
let d = (*f - *c).length();
assert!(
d < 1.0e-5,
"expected near-identity at high spp, got diff {d}"
);
}
}
#[test]
fn taper_strength_decreases_monotonically_with_spp() {
let n0 = 4.0;
let mut prev = taper_strength(0, n0);
assert!(
(prev - 1.0).abs() < 1.0e-6,
"taper at spp=0 should be exactly 1.0"
);
for spp in [1u32, 4, 16, 64, 256, 1024, 100_000] {
let t = taper_strength(spp, n0);
assert!(
t < prev,
"taper must strictly decrease as spp grows (spp={spp})"
);
assert!(t > 0.0, "taper must stay positive (spp={spp})");
prev = t;
}
}
#[test]
fn uniform_image_is_unchanged() {
let width = 20;
let height = 20;
let len = width * height;
let constant = Vec3::new(0.37, 0.61, 0.14);
let mut rng = Xorshift32::new(42);
let mut facet_id = vec![0i32; len];
let mut depth = vec![0.0f32; len];
let mut normal = vec![Vec3::Z; len];
for i in 0..len {
facet_id[i] = (rng.next_u32() % 4).cast_signed();
depth[i] = rng.next_f32();
let jitter = Vec3::new(rng.next_signed(), rng.next_signed(), 1.0).normalize();
normal[i] = jitter;
}
let color = vec![constant; len];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
for v in &filtered {
let d = (*v - constant).length();
assert!(
d < 1.0e-4,
"uniform image must be unchanged by filtering (drift {d})"
);
}
}
#[test]
fn filtering_is_deterministic() {
let width = 24;
let height = 24;
let len = width * height;
let mut rng = Xorshift32::new(7);
let mut color = vec![Vec3::ZERO; len];
let mut facet_id = vec![0i32; len];
let mut normal = vec![Vec3::Z; len];
let mut depth = vec![0.0f32; len];
for i in 0..len {
color[i] = Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32());
facet_id[i] = (rng.next_u32() % 5).cast_signed();
depth[i] = rng.next_f32();
normal[i] = Vec3::new(rng.next_signed() * 0.1, rng.next_signed() * 0.1, 1.0).normalize();
}
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 3,
path_sig: None,
};
let params = AtrousParams::default();
let mut denoiser_a = AtrousDenoiser::new();
let out_a = denoiser_a.denoise(&inputs, ¶ms);
let mut denoiser_b = AtrousDenoiser::new();
let out_b = denoiser_b.denoise(&inputs, ¶ms);
assert_eq!(out_a.len(), out_b.len());
for (a, b) in out_a.iter().zip(out_b.iter()) {
assert_eq!(
a.x.to_bits(),
b.x.to_bits(),
"filtering must be bit-exact deterministic"
);
assert_eq!(a.y.to_bits(), b.y.to_bits());
assert_eq!(a.z.to_bits(), b.z.to_bits());
}
let out_c = denoiser_a.denoise(&inputs, ¶ms);
for (a, c) in out_a.iter().zip(out_c.iter()) {
assert_eq!(a.x.to_bits(), c.x.to_bits());
assert_eq!(a.y.to_bits(), c.y.to_bits());
assert_eq!(a.z.to_bits(), c.z.to_bits());
}
}
#[test]
fn one_by_one_image_does_not_panic() {
let color = [Vec3::new(0.3, 0.6, 0.9)];
let depth = [1.0f32];
let normal = [Vec3::Z];
let facet_id = [0i32];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width: 1,
height: 1,
spp: 2,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
assert_eq!(filtered.len(), 1);
assert!(all_finite(&filtered));
let d = (filtered[0] - color[0]).length();
assert!(
d < 1.0e-4,
"single-pixel image should trivially reproduce its own color"
);
}
#[test]
fn zero_by_zero_image_does_not_panic() {
let color: [Vec3; 0] = [];
let depth: [f32; 0] = [];
let normal: [Vec3; 0] = [];
let facet_id: [i32; 0] = [];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width: 0,
height: 0,
spp: 0,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
assert_eq!(filtered, [] as [Vec3; 0]);
}
#[test]
fn zero_samples_does_not_panic_or_produce_nan() {
let width = 8;
let height = 8;
let len = width * height;
let mut rng = Xorshift32::new(2024);
let color: Vec<Vec3> = (0..len)
.map(|_| Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32()))
.collect();
let depth = vec![0.5f32; len];
let normal = vec![Vec3::Z; len];
let facet_id = vec![0i32; len];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 0,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
assert_eq!(filtered.len(), len);
assert!(all_finite(&filtered));
}
#[test]
fn all_black_buffer_does_not_panic_or_produce_nan() {
let width = 10;
let height = 10;
let len = width * height;
let color = vec![Vec3::ZERO; len];
let depth = vec![0.0f32; len];
let normal = vec![Vec3::ZERO; len]; let facet_id = vec![0i32; len];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 5,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
assert_eq!(filtered.len(), len);
assert!(all_finite(&filtered));
for v in &filtered {
assert_eq!(*v, Vec3::ZERO);
}
}
#[test]
fn mismatched_buffer_lengths_degrade_gracefully() {
let width = 6;
let height = 6;
let len = width * height;
let color = vec![Vec3::new(1.0, 1.0, 1.0); len];
let depth = vec![0.0f32; len / 2];
let normal = vec![Vec3::Z; len / 2];
let facet_id = vec![0i32; len / 2];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, &AtrousParams::default());
assert_eq!(filtered.len(), len);
assert!(all_finite(&filtered));
}
#[test]
fn parallel_denoise_is_bit_identical_across_thread_counts() {
let width = 173;
let height = 101;
let len = width * height;
let mut rng = Xorshift32::new(0x5eed_c0de);
let mut color = vec![Vec3::ZERO; len];
let mut depth = vec![0.0f32; len];
let mut normal = vec![Vec3::Z; len];
let mut facet_id = vec![0i32; len];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
facet_id[idx] = i32::try_from((x / 6 + y / 4) % 7).expect("a remainder below 7");
depth[idx] = rng.next_f32() * 3.0;
normal[idx] =
Vec3::new(rng.next_signed() * 0.4, rng.next_signed() * 0.4, 1.0).normalize();
color[idx] = Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32());
}
}
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let params = AtrousParams::default();
let mut reference_denoiser = AtrousDenoiser::new();
let mut reference = Vec::new();
reference_denoiser.denoise_into_with_threads(&inputs, ¶ms, &mut reference, 1);
for threads in [2usize, 8, 16] {
let mut denoiser = AtrousDenoiser::new();
let mut out = Vec::new();
denoiser.denoise_into_with_threads(&inputs, ¶ms, &mut out, threads);
assert_eq!(out.len(), reference.len());
for (i, (a, b)) in reference.iter().zip(out.iter()).enumerate() {
assert_eq!(
a.x.to_bits(),
b.x.to_bits(),
"threads={threads}: pixel {i} x component differs"
);
assert_eq!(
a.y.to_bits(),
b.y.to_bits(),
"threads={threads}: pixel {i} y component differs"
);
assert_eq!(
a.z.to_bits(),
b.z.to_bits(),
"threads={threads}: pixel {i} z component differs"
);
}
}
let mut auto_denoiser = AtrousDenoiser::new();
let mut auto_out = Vec::new();
auto_denoiser.denoise_into(&inputs, ¶ms, &mut auto_out);
for (a, b) in reference.iter().zip(auto_out.iter()) {
assert_eq!(a.x.to_bits(), b.x.to_bits());
assert_eq!(a.y.to_bits(), b.y.to_bits());
assert_eq!(a.z.to_bits(), b.z.to_bits());
}
}
#[test]
fn zero_passes_param_does_not_panic() {
let width = 4;
let height = 4;
let len = width * height;
let color = vec![Vec3::new(0.2, 0.4, 0.6); len];
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let facet_id = vec![0i32; len];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let params = AtrousParams {
num_passes: 0,
..AtrousParams::default()
};
let mut denoiser = AtrousDenoiser::new();
let filtered = denoiser.denoise(&inputs, ¶ms);
assert_eq!(filtered.len(), len);
assert!(all_finite(&filtered));
}
#[test]
fn a_non_finite_texel_does_not_contaminate_its_neighbours() {
let width = 40;
let height = 40;
let len = width * height;
let constant = Vec3::new(0.4, 0.5, 0.6);
let mut color = vec![constant; len];
color[20 * width + 20] = Vec3::new(f32::NAN, 0.5, 0.6);
color[5 * width + 30] = Vec3::new(f32::INFINITY, f32::NEG_INFINITY, 0.1);
color[35 * width + 2] = Vec3::splat(f32::NAN);
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let facet_id = vec![0i32; len];
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let filtered = AtrousDenoiser::new().denoise(&inputs, &AtrousParams::default());
for (i, v) in filtered.iter().enumerate() {
let d = (*v - constant).length();
assert!(d < 1.0e-4, "pixel {i} is {v:?}, not the uniform color");
}
}
#[test]
fn background_pixels_are_copied_through_unfiltered() {
let width = 24;
let height = 24;
let len = width * height;
let split = 12;
let mut rng = Xorshift32::new(77);
let color: Vec<Vec3> = (0..len)
.map(|_| Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32()))
.collect();
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let facet_id: Vec<i32> = (0..len)
.map(|i| if i % width < split { -1 } else { 3 })
.collect();
let inputs = GBuffers {
color: &color,
depth: &depth,
normal: &normal,
facet_id: &facet_id,
width,
height,
spp: 1,
path_sig: None,
};
let filtered = AtrousDenoiser::new().denoise(&inputs, &AtrousParams::default());
let mut foreground_changed = false;
for i in 0..len {
if facet_id[i] < 0 {
assert_eq!(filtered[i], color[i], "background pixel {i} was altered");
} else {
foreground_changed |= filtered[i] != color[i];
}
}
assert!(foreground_changed, "the foreground must still be filtered");
}
#[test]
fn a_frame_with_no_samples_is_a_finite_black_frame() {
let (width, height) = (6u32, 5u32);
let len = (width * height) as usize;
let mut rng = Xorshift32::new(31);
let accum: Vec<Vec3> = (0..len)
.map(|_| Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32()))
.collect();
let depth = vec![1.0f32; len];
let normal = vec![Vec3::Z; len];
let facet_id = vec![0i32; len];
let mut denoiser = AtrousDenoiser::new();
let (mut avg_color_buf, mut filtered_buf) = (Vec::new(), Vec::new());
let rgba = denoise_and_tonemap_frame(
FirstHitSnapshot {
width,
height,
current_sample_count: 0,
accum_buffer: &accum,
first_hit_depth: &depth,
first_hit_normal: &normal,
first_hit_facet_id: &facet_id,
first_hit_path_sig: &[],
},
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg_color_buf,
filtered_buf: &mut filtered_buf,
},
);
assert!(all_finite(&filtered_buf));
let black = vec![Vec3::ZERO; len];
assert_eq!(rgba, tonemap_to_rgba(&black, 1.0));
assert!(
rgba.as_chunks::<4>()
.0
.iter()
.all(|px| px[..3] == [0, 0, 0]),
"a frame with no samples must be black"
);
}