mod pass;
use super::tonemap::effective_thread_count;
use glam::Vec3;
use pass::{SharedColors, TapConstants, run_passes, unit_normal_or_zero};
#[derive(Clone, Copy)]
pub struct GBuffers<'a> {
pub color: &'a [Vec3],
pub depth: &'a [f32],
pub normal: &'a [Vec3],
pub facet_id: &'a [i32],
pub width: usize,
pub height: usize,
pub spp: u32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AtrousParams {
pub num_passes: u32,
pub sigma_color: f32,
pub sigma_depth: f32,
pub normal_power: f32,
pub taper_reference_spp: f32,
pub taper_identity_epsilon: f32,
}
impl Default for AtrousParams {
fn default() -> Self {
Self {
num_passes: 5,
sigma_color: 0.35,
sigma_depth: 0.1,
normal_power: 64.0,
taper_reference_spp: 4.0,
taper_identity_epsilon: 0.02,
}
}
}
const SIGMA_EPS: f32 = 1.0e-8;
#[must_use]
pub fn taper_strength(spp: u32, taper_reference_spp: f32) -> f32 {
let n0 = taper_reference_spp.max(SIGMA_EPS);
1.0 / (1.0 + spp as f32 / n0).sqrt()
}
#[derive(Default)]
pub struct AtrousDenoiser {
buffers: [SharedColors; 2],
normal_n: Vec<Vec3>,
}
impl AtrousDenoiser {
#[must_use]
pub const fn new() -> Self {
Self {
buffers: [SharedColors::new(), SharedColors::new()],
normal_n: Vec::new(),
}
}
fn ensure_capacity(&mut self, len: usize) {
for buffer in &mut self.buffers {
buffer.resize(len);
}
if self.normal_n.len() != len {
self.normal_n.clear();
self.normal_n.resize(len, Vec3::ZERO);
}
}
#[must_use]
pub fn denoise(&mut self, inputs: &GBuffers<'_>, params: &AtrousParams) -> Vec<Vec3> {
let mut output = vec![Vec3::ZERO; inputs.color.len()];
self.denoise_into(inputs, params, &mut output);
output
}
pub fn denoise_into(
&mut self,
inputs: &GBuffers<'_>,
params: &AtrousParams,
output: &mut Vec<Vec3>,
) {
self.denoise_into_with_threads(inputs, params, output, 0);
}
pub fn denoise_into_with_threads(
&mut self,
inputs: &GBuffers<'_>,
params: &AtrousParams,
output: &mut Vec<Vec3>,
threads: usize,
) {
let len = inputs.width * inputs.height;
output.clear();
output.resize(len, Vec3::ZERO);
if len == 0 {
return;
}
let buffers_ok = inputs.color.len() == len
&& inputs.depth.len() == len
&& inputs.normal.len() == len
&& inputs.facet_id.len() == len;
if !buffers_ok {
let n = inputs.color.len().min(len);
output[..n].copy_from_slice(&inputs.color[..n]);
for v in &mut output[n..] {
*v = Vec3::ZERO;
}
return;
}
let taper = taper_strength(inputs.spp, params.taper_reference_spp);
let num_passes = params.num_passes.clamp(1, 8);
if taper < params.taper_identity_epsilon {
output.copy_from_slice(inputs.color);
return;
}
let sigma_color_effective = (params.sigma_color * taper).max(0.0);
let num_threads = effective_thread_count(threads);
self.ensure_capacity(len);
self.buffers[0].fill_from(inputs.color);
for (dst, &src_n) in self.normal_n.iter_mut().zip(inputs.normal.iter()) {
*dst = unit_normal_or_zero(src_n);
}
let sigma_depth = params.sigma_depth.max(SIGMA_EPS);
let sigma_color_sq = (sigma_color_effective * sigma_color_effective).max(SIGMA_EPS);
let tap = TapConstants {
normal_n: &self.normal_n,
neg_inv_sigma_depth: -1.0 / sigma_depth,
neg_inv_two_sigma_color_sq: -1.0 / (2.0 * sigma_color_sq),
normal_power: params.normal_power,
};
let result = run_passes(&self.buffers, inputs, &tap, num_passes, num_threads);
self.buffers[result].copy_to(output);
}
}
#[must_use]
pub fn atrous_denoise(inputs: &GBuffers<'_>, params: &AtrousParams) -> Vec<Vec3> {
AtrousDenoiser::new().denoise(inputs, params)
}
#[cfg(test)]
mod tests {
use super::{pass::cos_pow, *};
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
}
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 irregular_scene(width: usize, height: usize, seed: u32) -> (GBuffers<'static>, Vec<Vec3>) {
let len = width * height;
let mut rng = Xorshift32::new(seed);
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] = ((x / 5 + y / 7) % 6) as i32;
depth[idx] = rng.next_f32() * 4.0;
let jitter =
Vec3::new(rng.next_signed() * 0.3, rng.next_signed() * 0.3, 1.0).normalize();
normal[idx] = jitter;
color[idx] = Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32());
}
}
let color_s: &'static [Vec3] = Box::leak(color.clone().into_boxed_slice());
let depth_s: &'static [f32] = Box::leak(depth.into_boxed_slice());
let normal_s: &'static [Vec3] = Box::leak(normal.into_boxed_slice());
let facet_s: &'static [i32] = Box::leak(facet_id.into_boxed_slice());
(
GBuffers {
color: color_s,
depth: depth_s,
normal: normal_s,
facet_id: facet_s,
width,
height,
spp: 1,
},
color,
)
}
fn assert_bit_identical(a: &[Vec3], b: &[Vec3], context: &str) {
assert_eq!(a.len(), b.len(), "{context}: length mismatch");
for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
assert_eq!(
x.x.to_bits(),
y.x.to_bits(),
"{context}: pixel {i} x differs"
);
assert_eq!(
x.y.to_bits(),
y.y.to_bits(),
"{context}: pixel {i} y differs"
);
assert_eq!(
x.z.to_bits(),
y.z.to_bits(),
"{context}: pixel {i} z differs"
);
}
}
#[test]
fn passes_are_thread_count_invariant() {
let width = 137;
let height = 91;
let (g, color) = irregular_scene(width, height, 0x1234_5678);
let params = AtrousParams::default();
let sigma = params.sigma_color;
let normal_n: Vec<Vec3> = g.normal.iter().map(|&n| unit_normal_or_zero(n)).collect();
let tap = TapConstants {
normal_n: &normal_n,
neg_inv_sigma_depth: -1.0 / params.sigma_depth.max(SIGMA_EPS),
neg_inv_two_sigma_color_sq: -1.0 / (2.0 * (sigma * sigma).max(SIGMA_EPS)),
normal_power: params.normal_power,
};
let run = |threads: usize| {
let mut buffers = [SharedColors::new(), SharedColors::new()];
for buffer in &mut buffers {
buffer.resize(width * height);
}
buffers[0].fill_from(&color);
let result = run_passes(&buffers, &g, &tap, 3, threads);
let mut out = vec![Vec3::ZERO; width * height];
buffers[result].copy_to(&mut out);
out
};
let reference = run(1);
for threads in [2usize, 3, 8, 16, 200] {
assert_bit_identical(&reference, &run(threads), &format!("threads={threads}"));
}
}
#[test]
fn cos_pow_matches_powf_within_tolerance() {
for &power in &[1.0f32, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0] {
for &base in &[0.0f32, 0.1, 0.37, 0.5, 0.9, 1.0] {
let fast = cos_pow(base, power);
let reference = base.powf(power);
let diff = (fast - reference).abs();
assert!(
diff <= reference.abs().mul_add(1.0e-5, 1.0e-6),
"cos_pow({base}, {power}) = {fast}, powf = {reference}, diff = {diff}"
);
}
}
for &power in &[5.0f32, 60.0] {
for &base in &[0.0f32, 0.3, 0.7, 1.0] {
assert_eq!(cos_pow(base, power), base.powf(power));
}
}
}
#[test]
fn denoise_into_is_thread_count_invariant() {
let width = 113;
let height = 67;
let (g, _color) = irregular_scene(width, height, 0xabcd_ef01);
let params = AtrousParams::default();
let mut denoiser = AtrousDenoiser::new();
let mut reference = Vec::new();
denoiser.denoise_into_with_threads(&g, ¶ms, &mut reference, 1);
for threads in [2usize, 4, 8, 16] {
let mut denoiser = AtrousDenoiser::new();
let mut out = Vec::new();
denoiser.denoise_into_with_threads(&g, ¶ms, &mut out, threads);
assert_bit_identical(&reference, &out, &format!("threads={threads}"));
}
let mut denoiser = AtrousDenoiser::new();
let mut out = Vec::new();
denoiser.denoise_into(&g, ¶ms, &mut out);
assert_bit_identical(&reference, &out, "auto thread count");
}
#[test]
fn a_reused_denoiser_matches_a_fresh_one_across_size_changes() {
let params = AtrousParams::default();
let mut reused = AtrousDenoiser::new();
for (width, height, seed) in [(40, 30, 1_u32), (17, 53, 2), (40, 30, 3)] {
let (g, _color) = irregular_scene(width, height, seed);
let mut from_reused = Vec::new();
reused.denoise_into_with_threads(&g, ¶ms, &mut from_reused, 4);
let mut from_fresh = Vec::new();
AtrousDenoiser::new().denoise_into_with_threads(&g, ¶ms, &mut from_fresh, 4);
assert_bit_identical(&from_fresh, &from_reused, &format!("{width}x{height}"));
}
}
}