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_geom_single_thread(
width: u32,
height: u32,
camera: &Camera,
geom: StoneGeometry<'_>,
n_d: f32,
) -> GuideBuffers {
generate_with_threads(
GuideJob {
width,
height,
camera,
planes: geom.planes,
tools: geom.tools,
n_d,
cancel: &AtomicBool::new(false),
},
1,
)
.expect("an unset cancellation flag cannot cancel guide generation")
}
#[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;