use crate::{
geometry::{
plane::GpuFacetPlane,
tool::{StoneGeometry, ToolPrimitive},
},
optics::raytracer::{
Camera, HitRecord, Ray, build_plane_soa, intersect_stone::intersect_stone_soa,
},
simd::PlanesSoA32,
};
use glam::Vec3;
use std::{
sync::{
Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
thread,
};
#[derive(Debug, Clone)]
pub struct GuideBuffers {
pub depth: Vec<f32>,
pub normal: Vec<Vec3>,
pub facet_id: Vec<i32>,
pub path_sig: Vec<u32>,
}
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],
path_sig: vec![0; pixel_count],
}
}
}
#[must_use]
pub fn generate_guide_buffers(
width: u32,
height: u32,
camera: &Camera,
planes: &[GpuFacetPlane],
) -> GuideBuffers {
generate_guide_buffers_with_index(width, height, camera, planes, 0.0)
}
#[must_use]
pub fn generate_guide_buffers_with_index(
width: u32,
height: u32,
camera: &Camera,
planes: &[GpuFacetPlane],
n_d: f32,
) -> GuideBuffers {
generate_guide_buffers_cancellable_with_index(
width,
height,
camera,
planes,
n_d,
&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_geom(
width: u32,
height: u32,
camera: &Camera,
geom: StoneGeometry<'_>,
) -> GuideBuffers {
generate_guide_buffers_geom_with_index(width, height, camera, geom, 0.0)
}
#[must_use]
pub fn generate_guide_buffers_geom_with_index(
width: u32,
height: u32,
camera: &Camera,
geom: StoneGeometry<'_>,
n_d: f32,
) -> GuideBuffers {
generate_guide_buffers_cancellable_geom_with_index(
width,
height,
camera,
geom,
n_d,
&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> {
generate_guide_buffers_cancellable_with_index(width, height, camera, planes, 0.0, cancel)
}
#[must_use]
pub fn generate_guide_buffers_cancellable_with_index(
width: u32,
height: u32,
camera: &Camera,
planes: &[GpuFacetPlane],
n_d: f32,
cancel: &AtomicBool,
) -> Option<GuideBuffers> {
generate_guide_buffers_cancellable_geom_with_index(
width,
height,
camera,
StoneGeometry::planes_only(planes),
n_d,
cancel,
)
}
#[must_use]
pub fn generate_guide_buffers_cancellable_geom(
width: u32,
height: u32,
camera: &Camera,
geom: StoneGeometry<'_>,
cancel: &AtomicBool,
) -> Option<GuideBuffers> {
generate_guide_buffers_cancellable_geom_with_index(width, height, camera, geom, 0.0, cancel)
}
#[must_use]
pub fn generate_guide_buffers_cancellable_geom_with_index(
width: u32,
height: u32,
camera: &Camera,
geom: StoneGeometry<'_>,
n_d: f32,
cancel: &AtomicBool,
) -> Option<GuideBuffers> {
let job = GuideJob {
width,
height,
camera,
planes: geom.planes,
tools: geom.tools,
n_d,
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 {
generate_guide_buffers_into_with_index(width, height, camera, planes, 0.0, cancel, out)
}
#[must_use]
pub fn generate_guide_buffers_into_with_index(
width: u32,
height: u32,
camera: &Camera,
planes: &[GpuFacetPlane],
n_d: f32,
cancel: &AtomicBool,
out: &mut GuideBuffers,
) -> bool {
generate_guide_buffers_into_geom_with_index(
width,
height,
camera,
StoneGeometry::planes_only(planes),
n_d,
cancel,
out,
)
}
#[must_use]
pub fn generate_guide_buffers_into_geom(
width: u32,
height: u32,
camera: &Camera,
geom: StoneGeometry<'_>,
cancel: &AtomicBool,
out: &mut GuideBuffers,
) -> bool {
generate_guide_buffers_into_geom_with_index(width, height, camera, geom, 0.0, cancel, out)
}
#[must_use]
pub fn generate_guide_buffers_into_geom_with_index(
width: u32,
height: u32,
camera: &Camera,
geom: StoneGeometry<'_>,
n_d: f32,
cancel: &AtomicBool,
out: &mut GuideBuffers,
) -> bool {
let job = GuideJob {
width,
height,
camera,
planes: geom.planes,
tools: geom.tools,
n_d,
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],
tools: &'a [ToolPrimitive],
n_d: f32,
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(),
path_sig: 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();
out.path_sig.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);
out.path_sig.resize(pixel_count, 0);
let soa = build_plane_soa(planes);
let soa = &soa;
let threads = num_threads.max(1).min(height as usize);
if threads <= 1 {
fill_rows(
job,
soa,
0,
&mut out.depth,
&mut out.normal,
&mut out.facet_id,
&mut out.path_sig,
);
} else {
fill_rows_dynamic(job, soa, threads, out);
}
!cancel.load(Ordering::Relaxed)
}
type GuideRowSlices<'a> = (&'a mut [f32], &'a mut [Vec3], &'a mut [i32], &'a mut [u32]);
fn fill_rows_dynamic(job: GuideJob<'_>, soa: &PlanesSoA32, threads: usize, out: &mut GuideBuffers) {
let width = job.width as usize;
let rows: Vec<Option<GuideRowSlices<'_>>> = out
.depth
.chunks_mut(width)
.zip(out.normal.chunks_mut(width))
.zip(out.facet_id.chunks_mut(width))
.zip(out.path_sig.chunks_mut(width))
.map(|(((depth, normal), facet), sig)| Some((depth, normal, facet, sig)))
.collect();
let row_count = rows.len();
let rows = Mutex::new(rows);
let next_row = AtomicUsize::new(0);
thread::scope(|s| {
for _ in 0..threads {
let (rows, next_row) = (&rows, &next_row);
s.spawn(move || {
loop {
let y = next_row.fetch_add(1, Ordering::Relaxed);
if y >= row_count || job.cancel.load(Ordering::Relaxed) {
break;
}
let claimed = rows
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)[y]
.take();
let Some((depth, normal, facet, sig)) = claimed else {
break;
};
fill_rows(job, soa, y, depth, normal, facet, sig);
}
});
}
});
}
pub const SIGNATURE_BOUNCES: usize = 3;
const SIG_EXIT_BIT: u32 = 0x8000;
const SIG_ESCAPE: u32 = 0xFFFF;
const SIG_SELF_HIT_EPS: f32 = 1.0e-4;
#[inline]
const fn fnv_mix(mut hash: u32, word: u32) -> u32 {
let mut shift = 0;
while shift < 32 {
hash ^= (word >> shift) & 0xFF;
hash = hash.wrapping_mul(0x0100_0193);
shift += 8;
}
hash
}
#[inline]
#[allow(
clippy::suboptimal_flops,
reason = "a fused multiply-add rounds once where wasm32 rounds twice, so x86 and wasm \
would hash different signatures for the same pixel"
)]
fn dot3(a: Vec3, b: Vec3) -> f32 {
a.x * b.x + a.y * b.y + a.z * b.z
}
#[inline]
fn unit(v: Vec3) -> Vec3 {
let len = dot3(v, v).sqrt();
if len > 0.0 { v / len } else { v }
}
#[allow(
clippy::suboptimal_flops,
reason = "the Snell and reflection arithmetic must stay unfused: an FMA would change \
the refracted direction in the last bit and so the facet hash on x86 vs wasm32"
)]
fn path_signature(
ray: Ray,
first: &HitRecord,
soa: &PlanesSoA32,
tools: &[ToolPrimitive],
n_d: f32,
) -> u32 {
if n_d.is_nan() || n_d <= 1.0 {
return 0;
}
let mut hash = fnv_mix(
0x811c_9dc5,
u32::try_from(first.facet_idx).expect("the facet index fits u32"),
);
let incident = unit(ray.dir);
let cos_i = (-dot3(incident, first.normal)).max(0.0);
let eta = 1.0 / n_d;
let k = 1.0 - eta * eta * (1.0 - cos_i * cos_i);
if k <= 0.0 {
return fnv_mix(hash, SIG_ESCAPE).max(1);
}
let mut dir = unit(incident * eta + first.normal * (eta * cos_i - k.sqrt()));
let mut origin = ray.origin + ray.dir * first.t;
for _ in 0..SIGNATURE_BOUNCES {
let probe = Ray {
origin: origin + dir * SIG_SELF_HIT_EPS,
dir,
};
let Some(hit) = intersect_stone_soa(probe, soa, soa.len(), tools) else {
hash = fnv_mix(hash, SIG_ESCAPE);
break;
};
origin = probe.origin + dir * hit.t;
let facet = u32::try_from(hit.facet_idx & 0x7FFF).expect("masked to 15 bits");
let cos_t = dot3(dir, hit.normal);
let sin_sq = n_d * n_d * (1.0 - cos_t * cos_t);
if sin_sq > 1.0 {
hash = fnv_mix(hash, facet);
dir = unit(dir - hit.normal * (2.0 * cos_t));
} else {
hash = fnv_mix(hash, facet | SIG_EXIT_BIT);
break;
}
}
hash.max(1)
}
fn fill_rows(
job: GuideJob<'_>,
soa: &PlanesSoA32,
start_y: usize,
depth: &mut [f32],
normal: &mut [Vec3],
facet_id: &mut [i32],
path_sig: &mut [u32],
) {
let width = job.width as usize;
let rows = depth
.chunks_mut(width)
.zip(normal.chunks_mut(width))
.zip(facet_id.chunks_mut(width))
.zip(path_sig.chunks_mut(width));
for (local_y, (((depth_row, normal_row), facet_row), sig_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())
.zip(sig_row.iter_mut());
for (x, (((depth_out, normal_out), facet_out), sig_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_stone_soa(ray, soa, soa.len(), job.tools);
*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| {
i32::try_from(h.facet_idx).expect("the facet index fits i32")
});
*sig_out = hit.map_or(0, |h| path_signature(ray, &h, soa, job.tools, job.n_d));
}
}
}
#[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 || usize::try_from(id).is_ok_and(|index| index < 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,
tools: &[],
n_d: 0.0,
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"
);
}
#[test]
fn guide_buffers_with_no_tools_match_the_plane_only_entry_point_bit_for_bit() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let old = generate_guide_buffers(16, 12, &camera, &planes);
let new = generate_guide_buffers_geom(16, 12, &camera, StoneGeometry::planes_only(&planes));
assert_eq!(old.facet_id, new.facet_id);
let bits = |b: &GuideBuffers| -> Vec<u32> { b.depth.iter().map(|d| d.to_bits()).collect() };
assert_eq!(bits(&old), bits(&new));
}
#[test]
fn without_an_index_the_signature_is_all_zeros_and_other_buffers_are_unchanged() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let plain = generate_guide_buffers(24, 16, &camera, &planes);
assert_eq!(plain.path_sig, vec![0; 24 * 16]);
let zero = generate_guide_buffers_with_index(24, 16, &camera, &planes, 0.0);
assert_eq!(zero.path_sig, plain.path_sig);
let indexed = generate_guide_buffers_with_index(24, 16, &camera, &planes, 2.417);
assert_eq!(indexed.facet_id, plain.facet_id);
assert_eq!(indexed.normal, plain.normal);
for (a, b) in indexed.depth.iter().zip(&plain.depth) {
assert_eq!(a.to_bits(), b.to_bits());
}
}
#[test]
fn signature_is_zero_exactly_on_misses() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let guides = generate_guide_buffers_with_index(48, 32, &camera, &planes, 2.417);
assert!(guides.facet_id.contains(&-1), "fixture needs a miss pixel");
assert!(
guides.facet_id.iter().any(|&id| id >= 0),
"fixture needs a hit pixel"
);
for (i, &id) in guides.facet_id.iter().enumerate() {
assert_eq!(guides.path_sig[i] == 0, id < 0, "pixel {i}, facet {id}");
}
}
#[test]
fn signature_is_identical_for_one_two_and_eight_threads() {
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: 61,
height: 37,
camera: &camera,
planes: &planes,
tools: &[],
n_d: 2.417,
cancel: &cancel,
};
let single = generate_with_threads(job, 1).expect("never cancelled");
let distinct: std::collections::HashSet<u32> = single.path_sig.iter().copied().collect();
assert!(
distinct.len() > 4,
"a brilliant seen at this pose must show several interior regions"
);
for threads in [2, 8] {
let multi = generate_with_threads(job, threads).expect("never cancelled");
assert_eq!(multi.path_sig, single.path_sig, "{threads} threads");
}
}
#[test]
fn signature_regions_change_with_the_index() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let diamond = generate_guide_buffers_with_index(64, 48, &camera, &planes, 2.417);
let glass = generate_guide_buffers_with_index(64, 48, &camera, &planes, 1.52);
assert_ne!(diamond.path_sig, glass.path_sig);
assert_eq!(diamond.facet_id, glass.facet_id);
}
#[test]
fn signature_survives_buffer_reuse() {
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_with_index(24, 16, &old_camera, &planes, 2.417);
assert!(generate_guide_buffers_into_with_index(
16,
12,
&camera,
&planes,
2.417,
&cancel,
&mut reused
));
let fresh = generate_guide_buffers_with_index(16, 12, &camera, &planes, 2.417);
assert_eq!(reused.path_sig, fresh.path_sig);
}
#[test]
fn guide_buffers_encode_tool_facet_ids_above_the_plane_count() {
let planes = StandardGemCuts::standard_round_brilliant();
let tools = [ToolPrimitive::cylinder(
Vec3::new(0.0, 0.44, 0.0),
Vec3::X,
0.3,
2.0,
)];
let camera = Camera::new(0.0, 1.5, 4.0, 20.0);
let guides = generate_guide_buffers_geom(
24,
24,
&camera,
StoneGeometry {
planes: &planes,
tools: &tools,
},
);
let first_tool_id = i32::try_from(planes.len()).expect("the plane count fits i32");
assert!(
guides.facet_id.contains(&first_tool_id),
"the groove floor must appear as tool facet {first_tool_id}"
);
assert!(guides.facet_id.iter().all(|&id| id <= first_tool_id));
}
}