use super::compositing::{AccumSnapshot, PoseAndGeometry, render_merged_frame};
use crate::bridge::{
frame_cache::guide_pass::{
GuideBuffers, GuideCache, GuideKey, generate_guide_buffers_cancellable,
},
render_thread::DenoiseScratch,
};
use glam::Vec3;
use indicatrix::{
geometry::plane::GpuFacetPlane, optics::raytracer::Camera, renderer::denoise::AtrousDenoiser,
};
use std::sync::{Arc, Mutex, PoisonError, atomic::AtomicBool};
pub(super) struct PendingGuideGeneration {
pub(super) key: GuideKey,
pub(super) cancel: Arc<AtomicBool>,
result: Arc<Mutex<Option<GuideBuffers>>>,
}
pub(super) fn spawn_guide_generation(
key: GuideKey,
camera: Camera,
planes: Vec<GpuFacetPlane>,
width: u32,
height: u32,
) -> PendingGuideGeneration {
let cancel = Arc::new(AtomicBool::new(false));
let result = Arc::new(Mutex::new(None));
let cancel_worker = Arc::clone(&cancel);
let result_worker = Arc::clone(&result);
std::thread::spawn(move || {
if let Some(buffers) =
generate_guide_buffers_cancellable(width, height, &camera, &planes, &cancel_worker)
{
*result_worker.lock().unwrap_or_else(PoisonError::into_inner) = Some(buffers);
}
});
PendingGuideGeneration {
key,
cancel,
result,
}
}
pub(super) fn adopt_ready_guides(
desired_key: &GuideKey,
guide_cache: &mut GuideCache,
pending: Option<&PendingGuideGeneration>,
) -> bool {
if guide_cache.matches_key(desired_key) {
return true;
}
let Some(pending) = pending else {
return false;
};
if pending.key != *desired_key {
return false;
}
let mut slot = pending
.result
.lock()
.unwrap_or_else(PoisonError::into_inner);
let Some(buffers) = slot.take() else {
return false;
};
drop(slot);
guide_cache.adopt(desired_key.clone(), buffers);
true
}
pub(super) struct PendingDenoiseGeneration {
pub(super) key: GuideKey,
result: Arc<Mutex<Option<Vec<u8>>>>,
}
pub(super) struct DenoiseGenerationJob {
pub(super) width: u32,
pub(super) height: u32,
pub(super) samples_done: u32,
pub(super) buffer: Vec<Vec3>,
pub(super) guides: GuideBuffers,
pub(super) yaw: f32,
pub(super) pitch: f32,
pub(super) distance: f32,
pub(super) planes: Vec<GpuFacetPlane>,
}
pub(super) fn spawn_denoise_generation(
key: GuideKey,
job: DenoiseGenerationJob,
) -> PendingDenoiseGeneration {
let result = Arc::new(Mutex::new(None));
let result_worker = Arc::clone(&result);
let key_for_thread = key.clone();
std::thread::spawn(move || {
let mut guide_cache = GuideCache::new();
guide_cache.adopt(key_for_thread, job.guides);
let mut denoiser = AtrousDenoiser::new();
let mut avg_color_buf = Vec::new();
let mut filtered_buf = Vec::new();
let bytes = render_merged_frame(
AccumSnapshot {
width: job.width,
height: job.height,
samples_done: job.samples_done,
buffer: &job.buffer,
},
true,
PoseAndGeometry {
yaw: job.yaw,
pitch: job.pitch,
distance: job.distance,
planes: &job.planes,
},
&mut guide_cache,
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg_color_buf,
filtered_buf: &mut filtered_buf,
},
);
*result_worker.lock().unwrap_or_else(PoisonError::into_inner) = Some(bytes);
});
PendingDenoiseGeneration { key, result }
}
pub(super) fn adopt_ready_denoise(
desired_key: &GuideKey,
pending: Option<&PendingDenoiseGeneration>,
) -> Option<Vec<u8>> {
let pending = pending?;
if pending.key != *desired_key {
return None;
}
let mut slot = pending
.result
.lock()
.unwrap_or_else(PoisonError::into_inner);
slot.take()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::frame_cache::guide_pass::generate_guide_buffers;
use indicatrix::geometry::cuts::StandardGemCuts;
fn ready_pending(key: GuideKey, buffers: GuideBuffers) -> PendingGuideGeneration {
PendingGuideGeneration {
key,
cancel: Arc::new(AtomicBool::new(false)),
result: Arc::new(Mutex::new(Some(buffers))),
}
}
fn ready_denoise_pending(key: GuideKey, bytes: Vec<u8>) -> PendingDenoiseGeneration {
PendingDenoiseGeneration {
key,
result: Arc::new(Mutex::new(Some(bytes))),
}
}
fn in_flight_denoise_pending(key: GuideKey) -> PendingDenoiseGeneration {
PendingDenoiseGeneration {
key,
result: Arc::new(Mutex::new(None)),
}
}
#[test]
fn adopt_ready_denoise_returns_none_when_nothing_is_pending() {
let planes = StandardGemCuts::standard_round_brilliant();
let key = GuideCache::key_for(6, 5, 0.60, 0.45, 2.4, &planes);
assert_eq!(adopt_ready_denoise(&key, None), None);
}
#[test]
fn adopt_ready_denoise_returns_none_while_still_in_flight() {
let planes = StandardGemCuts::standard_round_brilliant();
let key = GuideCache::key_for(6, 5, 0.60, 0.45, 2.4, &planes);
let pending = in_flight_denoise_pending(key.clone());
assert_eq!(
adopt_ready_denoise(&key, Some(&pending)),
None,
"a generation that hasn't produced a result yet is 'not ready', not an error"
);
}
#[test]
fn adopt_ready_denoise_takes_the_result_once_ready_and_matching() {
let planes = StandardGemCuts::standard_round_brilliant();
let key = GuideCache::key_for(6, 5, 0.60, 0.45, 2.4, &planes);
let bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
let pending = ready_denoise_pending(key.clone(), bytes.clone());
assert_eq!(
adopt_ready_denoise(&key, Some(&pending)),
Some(bytes),
"a finished generation for the exact pose on screen must be adopted"
);
assert_eq!(
adopt_ready_denoise(&key, Some(&pending)),
None,
"the result is taken, not cloned -- a second read must not resurrect it"
);
}
#[test]
fn adopt_ready_denoise_rejects_a_result_for_a_different_pose() {
let planes = StandardGemCuts::standard_round_brilliant();
let stale_key = GuideCache::key_for(6, 5, 0.10, 0.45, 2.4, &planes);
let pending = ready_denoise_pending(stale_key, vec![9u8, 9, 9, 9]);
let current_key = GuideCache::key_for(6, 5, 0.60, 0.45, 2.4, &planes);
assert_eq!(
adopt_ready_denoise(¤t_key, Some(&pending)),
None,
"a background result for a pose other than the one displayed must never be \
adopted -- rejection is structural (a key comparison), not timing-dependent"
);
}
#[test]
fn a_superseded_generations_late_result_does_not_overwrite_newer_guides() {
let planes = StandardGemCuts::standard_round_brilliant();
let (width, height) = (6u32, 5u32);
let current_key = GuideCache::key_for(width, height, 0.90, 0.45, 2.4, &planes);
let current_camera = Camera::new(0.90, 0.45, 2.4, 42.0);
let current_buffers = generate_guide_buffers(width, height, ¤t_camera, &planes);
let mut guide_cache = GuideCache::new();
guide_cache.adopt(current_key.clone(), current_buffers.clone());
let old_key = GuideCache::key_for(width, height, 0.10, 0.45, 2.4, &planes);
let old_camera = Camera::new(0.10, 0.45, 2.4, 42.0);
let old_buffers = generate_guide_buffers(width, height, &old_camera, &planes);
let superseded = ready_pending(old_key, old_buffers);
let adopted = adopt_ready_guides(¤t_key, &mut guide_cache, Some(&superseded));
assert!(
adopted,
"the cache already had the correct guides for the current pose"
);
assert!(
guide_cache.matches_key(¤t_key),
"a superseded generation's late result for an older pose must never \
overwrite the current pose's already-adopted guides"
);
assert_eq!(
guide_cache
.ensure(width, height, 0.90, 0.45, 2.4, &planes)
.depth,
current_buffers.depth,
"the buffers in the cache must still be the current pose's, not the stale \
generation's"
);
}
}