use crate::{
geometry::plane::GpuFacetPlane,
optics::raytracer::{Camera, build_plane_soa, intersect_polyhedron_soa},
simd::PlanesSoA32,
};
use glam::Vec3;
use std::{
sync::atomic::{AtomicBool, Ordering},
thread,
};
#[derive(Debug, Clone)]
pub struct GuideBuffers {
pub depth: Vec<f32>,
pub normal: Vec<Vec3>,
pub facet_id: Vec<i32>,
}
impl GuideBuffers {
#[must_use]
pub fn miss(width: u32, height: u32) -> Self {
let pixel_count = (width as usize) * (height as usize);
Self {
depth: vec![1.0e6; pixel_count],
normal: vec![Vec3::ZERO; pixel_count],
facet_id: vec![-1; pixel_count],
}
}
}
#[must_use]
pub fn generate_guide_buffers(
width: u32,
height: u32,
camera: &Camera,
planes: &[GpuFacetPlane],
) -> GuideBuffers {
generate_guide_buffers_cancellable(width, height, camera, planes, &AtomicBool::new(false))
.expect("a cancel flag that is never set to true never yields a cancelled result")
}
#[must_use]
pub fn generate_guide_buffers_cancellable(
width: u32,
height: u32,
camera: &Camera,
planes: &[GpuFacetPlane],
cancel: &AtomicBool,
) -> Option<GuideBuffers> {
let job = GuideJob {
width,
height,
camera,
planes,
cancel,
};
generate_with_threads(job, auto_thread_count())
}
#[must_use]
pub fn generate_guide_buffers_into(
width: u32,
height: u32,
camera: &Camera,
planes: &[GpuFacetPlane],
cancel: &AtomicBool,
out: &mut GuideBuffers,
) -> bool {
let job = GuideJob {
width,
height,
camera,
planes,
cancel,
};
fill_with_threads(job, auto_thread_count(), out)
}
#[cfg(target_arch = "wasm32")]
const fn auto_thread_count() -> usize {
1
}
#[cfg(not(target_arch = "wasm32"))]
fn auto_thread_count() -> usize {
crate::renderer::tonemap::effective_thread_count(0)
}
#[derive(Clone, Copy)]
struct GuideJob<'a> {
width: u32,
height: u32,
camera: &'a Camera,
planes: &'a [GpuFacetPlane],
cancel: &'a AtomicBool,
}
fn generate_with_threads(job: GuideJob<'_>, num_threads: usize) -> Option<GuideBuffers> {
let mut buffers = GuideBuffers {
depth: Vec::new(),
normal: Vec::new(),
facet_id: Vec::new(),
};
fill_with_threads(job, num_threads, &mut buffers).then_some(buffers)
}
fn fill_with_threads(job: GuideJob<'_>, num_threads: usize, out: &mut GuideBuffers) -> bool {
let GuideJob {
width,
height,
cancel,
planes,
..
} = job;
let pixel_count = (width as usize) * (height as usize);
if pixel_count == 0 {
out.depth.clear();
out.normal.clear();
out.facet_id.clear();
return true;
}
if cancel.load(Ordering::Relaxed) {
return false;
}
out.depth.resize(pixel_count, 1.0e6);
out.normal.resize(pixel_count, Vec3::ZERO);
out.facet_id.resize(pixel_count, -1);
let soa = build_plane_soa(planes);
let soa = &soa;
let rows_per_chunk = (height as usize).div_ceil(num_threads.max(1));
if rows_per_chunk >= height as usize {
fill_rows(
job,
soa,
0,
&mut out.depth,
&mut out.normal,
&mut out.facet_id,
);
} else {
let chunk_len = rows_per_chunk * width as usize;
thread::scope(|s| {
let chunks = out
.depth
.chunks_mut(chunk_len)
.zip(out.normal.chunks_mut(chunk_len))
.zip(out.facet_id.chunks_mut(chunk_len));
for (chunk_idx, ((depth_chunk, normal_chunk), facet_chunk)) in chunks.enumerate() {
let start_y = chunk_idx * rows_per_chunk;
s.spawn(move || {
fill_rows(job, soa, start_y, depth_chunk, normal_chunk, facet_chunk);
});
}
});
}
!cancel.load(Ordering::Relaxed)
}
fn fill_rows(
job: GuideJob<'_>,
soa: &PlanesSoA32,
start_y: usize,
depth: &mut [f32],
normal: &mut [Vec3],
facet_id: &mut [i32],
) {
let width = job.width as usize;
let rows = depth
.chunks_mut(width)
.zip(normal.chunks_mut(width))
.zip(facet_id.chunks_mut(width));
for (local_y, ((depth_row, normal_row), facet_row)) in rows.enumerate() {
if job.cancel.load(Ordering::Relaxed) {
return;
}
let y = start_y + local_y;
let cells = depth_row
.iter_mut()
.zip(normal_row.iter_mut())
.zip(facet_row.iter_mut());
for (x, ((depth_out, normal_out), facet_out)) in cells.enumerate() {
let ray = job.camera.generate_ray(
x as f32,
y as f32,
job.width as f32,
job.height as f32,
0.0,
0.0,
);
let hit = intersect_polyhedron_soa(ray, soa);
*depth_out = hit.map_or(1.0e6, |h| h.t);
*normal_out = hit.map_or(Vec3::ZERO, |h| h.normal);
*facet_out = hit.map_or(-1, |h| h.facet_idx as i32);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::cuts::StandardGemCuts;
#[test]
fn generate_guide_buffers_is_correctly_sized_and_facet_bounded() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let guides = generate_guide_buffers(16, 12, &camera, &planes);
assert_eq!(guides.depth.len(), 16 * 12);
assert_eq!(guides.normal.len(), 16 * 12);
assert_eq!(guides.facet_id.len(), 16 * 12);
let centre = 6 * 16 + 8;
assert!(
guides.facet_id[centre] >= 0,
"centre pixel should hit the gem"
);
for &id in &guides.facet_id {
assert!(
id == -1 || (id as usize) < planes.len(),
"facet id {id} out of range for {} planes",
planes.len()
);
}
}
#[test]
fn generate_guide_buffers_handles_zero_area_without_panicking() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.0, 0.0, 2.4, 42.0);
let guides = generate_guide_buffers(0, 0, &camera, &planes);
assert_eq!(guides.depth.len(), 0);
assert_eq!(guides.normal.len(), 0);
assert_eq!(guides.facet_id.len(), 0);
}
#[test]
fn a_miss_pixel_gets_the_sentinel_depth_and_facet_id() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.0, 1.5, 50.0, 5.0);
let guides = generate_guide_buffers(4, 4, &camera, &planes);
assert!(
guides.facet_id.contains(&-1),
"expected at least one miss pixel at this camera distance/fov"
);
for (i, &id) in guides.facet_id.iter().enumerate() {
if id == -1 {
assert_eq!(guides.depth[i].to_bits(), 1.0e6_f32.to_bits());
assert_eq!(guides.normal[i], Vec3::ZERO);
}
}
}
#[test]
fn generate_guide_buffers_cancellable_matches_the_non_cancellable_version_when_never_cancelled()
{
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let cancel = AtomicBool::new(false);
let expected = generate_guide_buffers(16, 12, &camera, &planes);
let actual = generate_guide_buffers_cancellable(16, 12, &camera, &planes, &cancel)
.expect("an AtomicBool that's never set true must never yield a cancelled result");
assert_eq!(actual.depth, expected.depth);
assert_eq!(actual.facet_id, expected.facet_id);
}
#[test]
fn single_thread_and_multi_thread_paths_produce_identical_guides() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let cancel = AtomicBool::new(false);
let job = GuideJob {
width: 21,
height: 13,
camera: &camera,
planes: &planes,
cancel: &cancel,
};
let single = generate_with_threads(job, 1).expect("never cancelled");
assert!(
single.facet_id.iter().any(|&id| id >= 0),
"fixture must hit the gem somewhere"
);
let bits = |g: &GuideBuffers| -> Vec<u32> {
g.depth
.iter()
.copied()
.map(f32::to_bits)
.chain(g.normal.iter().flat_map(|n| n.to_array().map(f32::to_bits)))
.collect()
};
for threads in [2, 3, 7, 64] {
let multi = generate_with_threads(job, threads).expect("never cancelled");
assert_eq!(bits(&multi), bits(&single), "{threads} threads");
assert_eq!(multi.facet_id, single.facet_id, "{threads} threads");
}
}
#[test]
fn generating_into_a_reused_buffer_matches_a_fresh_allocation() {
let planes = StandardGemCuts::standard_round_brilliant();
let cancel = AtomicBool::new(false);
let old_camera = Camera::new(1.10, 0.20, 2.4, 42.0);
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let mut reused = generate_guide_buffers(24, 16, &old_camera, &planes);
assert!(generate_guide_buffers_into(
16,
12,
&camera,
&planes,
&cancel,
&mut reused
));
let fresh = generate_guide_buffers(16, 12, &camera, &planes);
assert_eq!(reused.facet_id, fresh.facet_id);
assert_eq!(reused.depth.len(), fresh.depth.len());
for (a, b) in reused.depth.iter().zip(&fresh.depth) {
assert_eq!(a.to_bits(), b.to_bits());
}
assert_eq!(reused.normal, fresh.normal);
}
#[test]
fn generate_guide_buffers_cancellable_returns_none_when_pre_cancelled() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let cancel = AtomicBool::new(true);
let result = generate_guide_buffers_cancellable(64, 64, &camera, &planes, &cancel);
assert!(
result.is_none(),
"a generation cancelled before it starts must not produce buffers"
);
}
}