use crate::bridge::{
frame_cache::guide_pass::GuideCache,
render_thread::{
DenoiseScratch, FirstHitSnapshot, denoise_and_tonemap_frame, tonemap_running_average,
},
};
use glam::Vec3;
use indicatrix::geometry::{
plane::GpuFacetPlane,
tool::{StoneGeometry, ToolPrimitive},
};
#[derive(Clone, Copy)]
pub(super) struct AccumSnapshot<'a> {
pub(super) width: u32,
pub(super) height: u32,
pub(super) samples_done: u32,
pub(super) buffer: &'a [Vec3],
}
#[derive(Clone, Copy)]
pub(super) struct PoseAndGeometry<'a> {
pub(super) yaw: f32,
pub(super) pitch: f32,
pub(super) distance: f32,
pub(super) planes: &'a [GpuFacetPlane],
pub(super) tools: &'a [ToolPrimitive],
pub(super) n_d: f32,
}
pub(super) fn render_merged_frame(
accum: AccumSnapshot<'_>,
denoise_enabled: bool,
pose: PoseAndGeometry<'_>,
guide_cache: &mut GuideCache,
scratch: &mut DenoiseScratch<'_>,
) -> Vec<u8> {
if !denoise_enabled {
return tonemap_running_average(
accum.width,
accum.height,
accum.samples_done,
accum.buffer,
);
}
let guides = guide_cache.ensure_geom(
accum.width,
accum.height,
pose.yaw,
pose.pitch,
pose.distance,
StoneGeometry {
planes: pose.planes,
tools: pose.tools,
},
pose.n_d,
);
denoise_and_tonemap_frame(
FirstHitSnapshot {
width: accum.width,
height: accum.height,
current_sample_count: accum.samples_done,
accum_buffer: accum.buffer,
first_hit_depth: &guides.depth,
first_hit_normal: &guides.normal,
first_hit_facet_id: &guides.facet_id,
first_hit_path_sig: &guides.path_sig,
},
scratch,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::frame_cache::guide_pass::generate_guide_buffers;
use indicatrix::{
geometry::cuts::StandardGemCuts,
optics::raytracer::{Camera, DEFAULT_FOV_DEG},
renderer::denoise::AtrousDenoiser,
};
fn synthetic_noisy_buffer(width: u32, height: u32) -> Vec<Vec3> {
(0..(width * height) as usize)
.map(|i| {
let x = (i % width as usize) as f32;
Vec3::new(1.0 + x, 0.5 * x, 0.1f32.mul_add(-x, 2.0))
})
.collect()
}
#[test]
fn render_merged_frame_skips_denoising_when_disabled() {
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (6u32, 5u32);
let buffer = synthetic_noisy_buffer(width, height);
let mut guide_cache = GuideCache::new();
let mut denoiser = AtrousDenoiser::new();
let mut avg = Vec::new();
let mut filtered = Vec::new();
let bytes = render_merged_frame(
AccumSnapshot {
width,
height,
samples_done: 4,
buffer: &buffer,
},
false,
PoseAndGeometry {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
planes: &planes,
tools: &[],
n_d: 0.0,
},
&mut guide_cache,
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg,
filtered_buf: &mut filtered,
},
);
assert_eq!(bytes, tonemap_running_average(width, height, 4, &buffer));
assert_eq!(
guide_cache.generation(),
0,
"denoising disabled must never trigger the guide prepass at all -- the \
user's toggle governs whether the (cheap, but not free) prepass runs, \
exactly like it already governs the local path"
);
}
#[test]
fn render_merged_frame_reuses_guides_across_repeated_calls_with_an_unchanged_pose() {
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (6u32, 5u32);
let buffer = synthetic_noisy_buffer(width, height);
let mut guide_cache = GuideCache::new();
let mut denoiser = AtrousDenoiser::new();
let mut avg = Vec::new();
let mut filtered = Vec::new();
for samples in [1u32, 2, 3] {
render_merged_frame(
AccumSnapshot {
width,
height,
samples_done: samples,
buffer: &buffer,
},
true,
PoseAndGeometry {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
planes: &planes,
tools: &[],
n_d: 0.0,
},
&mut guide_cache,
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg,
filtered_buf: &mut filtered,
},
);
}
assert_eq!(
guide_cache.generation(),
1,
"repeated redraws of the same in-progress remote render at an unchanged \
pose must reuse the cached guide buffers, not regenerate them per frame"
);
}
#[test]
fn render_merged_frame_regenerates_guides_when_the_pose_changes() {
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (6u32, 5u32);
let buffer = synthetic_noisy_buffer(width, height);
let mut guide_cache = GuideCache::new();
let mut denoiser = AtrousDenoiser::new();
let mut avg = Vec::new();
let mut filtered = Vec::new();
render_merged_frame(
AccumSnapshot {
width,
height,
samples_done: 1,
buffer: &buffer,
},
true,
PoseAndGeometry {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
planes: &planes,
tools: &[],
n_d: 0.0,
},
&mut guide_cache,
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg,
filtered_buf: &mut filtered,
},
);
render_merged_frame(
AccumSnapshot {
width,
height,
samples_done: 1,
buffer: &buffer,
},
true,
PoseAndGeometry {
yaw: 0.90,
pitch: 0.45,
distance: 2.4,
planes: &planes,
tools: &[],
n_d: 0.0,
},
&mut guide_cache,
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg,
filtered_buf: &mut filtered,
},
);
assert_eq!(
guide_cache.generation(),
2,
"a camera-pose change (a fresh drag settling into a new remote request) \
must invalidate the cached guides -- reusing a stale pose's guides would \
misalign depth/normal/facet-id against the new image's geometry"
);
}
#[test]
fn render_merged_frame_at_a_converged_sample_count_matches_a_plain_tonemap() {
const HIGH_SAMPLE_COUNT: u32 = 50_000;
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (6u32, 5u32);
let buffer = synthetic_noisy_buffer(width, height);
let mut guide_cache = GuideCache::new();
let mut denoiser = AtrousDenoiser::new();
let mut avg = Vec::new();
let mut filtered = Vec::new();
let high_total_bytes = render_merged_frame(
AccumSnapshot {
width,
height,
samples_done: HIGH_SAMPLE_COUNT,
buffer: &buffer,
},
true,
PoseAndGeometry {
yaw: 0.60,
pitch: 0.45,
distance: 2.4,
planes: &planes,
tools: &[],
n_d: 0.0,
},
&mut guide_cache,
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg,
filtered_buf: &mut filtered,
},
);
assert_eq!(
high_total_bytes,
tonemap_running_average(width, height, HIGH_SAMPLE_COUNT, &buffer),
"at a converged TRUE total, denoising the merged buffer must be an exact \
no-op, just like the local path's readback"
);
}
#[test]
fn render_merged_frame_at_a_low_sample_count_matches_denoise_and_tonemap_frame_directly() {
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (6u32, 5u32);
let buffer = synthetic_noisy_buffer(width, height);
let (yaw, pitch, distance) = (0.60, 0.45, 2.4);
let samples_done = 1;
let mut guide_cache = GuideCache::new();
let mut denoiser = AtrousDenoiser::new();
let mut avg = Vec::new();
let mut filtered = Vec::new();
let actual = render_merged_frame(
AccumSnapshot {
width,
height,
samples_done,
buffer: &buffer,
},
true,
PoseAndGeometry {
yaw,
pitch,
distance,
planes: &planes,
tools: &[],
n_d: 0.0,
},
&mut guide_cache,
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg,
filtered_buf: &mut filtered,
},
);
let camera = Camera::new(yaw, pitch, distance, DEFAULT_FOV_DEG);
let guides = generate_guide_buffers(width, height, &camera, &planes);
let mut expected_denoiser = AtrousDenoiser::new();
let mut expected_avg = Vec::new();
let mut expected_filtered = Vec::new();
let expected = denoise_and_tonemap_frame(
FirstHitSnapshot {
width,
height,
current_sample_count: samples_done,
accum_buffer: &buffer,
first_hit_depth: &guides.depth,
first_hit_normal: &guides.normal,
first_hit_facet_id: &guides.facet_id,
first_hit_path_sig: &guides.path_sig,
},
&mut DenoiseScratch {
denoiser: &mut expected_denoiser,
avg_color_buf: &mut expected_avg,
filtered_buf: &mut expected_filtered,
},
);
assert_eq!(
actual, expected,
"render_merged_frame must denoise via the exact same guides/mechanism a \
direct generate_guide_buffers + denoise_and_tonemap_frame call would \
produce for the identical pose and geometry"
);
}
}