use crate::bridge::render_thread::hash_planes;
use glam::Vec3;
use indicatrix::{
geometry::plane::GpuFacetPlane,
optics::raytracer::{Camera, intersect_polyhedron},
};
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 {
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> {
if width == 0 || height == 0 {
return Some(GuideBuffers::miss(width, height));
}
if cancel.load(Ordering::Relaxed) {
return None;
}
let mut buffers = GuideBuffers::miss(width, height);
let num_threads = thread::available_parallelism().map_or(8, std::num::NonZero::get);
let rows_per_chunk = (height as usize).div_ceil(num_threads);
thread::scope(|s| {
let chunks_depth: Vec<&mut [f32]> = buffers
.depth
.chunks_mut(rows_per_chunk * width as usize)
.collect();
let chunks_normal: Vec<&mut [Vec3]> = buffers
.normal
.chunks_mut(rows_per_chunk * width as usize)
.collect();
let chunks_facet: Vec<&mut [i32]> = buffers
.facet_id
.chunks_mut(rows_per_chunk * width as usize)
.collect();
let chunks = chunks_depth
.into_iter()
.zip(chunks_normal)
.zip(chunks_facet);
for (chunk_idx, ((depth_chunk, normal_chunk), facet_chunk)) in chunks.enumerate() {
let start_y = chunk_idx * rows_per_chunk;
let end_y = (start_y + rows_per_chunk).min(height as usize);
s.spawn(move || {
for y in start_y..end_y {
if cancel.load(Ordering::Relaxed) {
return;
}
let local_y = y - start_y;
let row_offset = local_y * width as usize;
for x in 0..(width as usize) {
let local_idx = row_offset + x;
let ray = camera.generate_ray(
x as f32,
y as f32,
width as f32,
height as f32,
0.0,
0.0,
);
let hit = intersect_polyhedron(ray, planes);
depth_chunk[local_idx] = hit.map_or(1.0e6, |h| h.t);
normal_chunk[local_idx] = hit.map_or(Vec3::ZERO, |h| h.normal);
facet_chunk[local_idx] = hit.map_or(-1, |h| h.facet_idx as i32);
}
}
});
}
});
if cancel.load(Ordering::Relaxed) {
None
} else {
Some(buffers)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GuideKey {
width: u32,
height: u32,
yaw: f32,
pitch: f32,
distance: f32,
planes_hash: u64,
}
#[derive(Debug)]
pub struct GuideCache {
key: Option<GuideKey>,
buffers: GuideBuffers,
generation: u64,
}
impl Default for GuideCache {
fn default() -> Self {
Self::new()
}
}
impl GuideCache {
#[must_use]
pub fn new() -> Self {
Self {
key: None,
buffers: GuideBuffers::miss(0, 0),
generation: 0,
}
}
#[cfg(test)]
#[must_use]
pub const fn generation(&self) -> u64 {
self.generation
}
pub fn ensure(
&mut self,
width: u32,
height: u32,
yaw: f32,
pitch: f32,
distance: f32,
planes: &[GpuFacetPlane],
) -> &GuideBuffers {
let key = Self::key_for(width, height, yaw, pitch, distance, planes);
if self.key.as_ref() != Some(&key) {
let camera = Camera::new(yaw, pitch, distance, 42.0);
self.buffers = generate_guide_buffers(width, height, &camera, planes);
self.key = Some(key);
self.generation += 1;
}
&self.buffers
}
#[must_use]
pub fn key_for(
width: u32,
height: u32,
yaw: f32,
pitch: f32,
distance: f32,
planes: &[GpuFacetPlane],
) -> GuideKey {
GuideKey {
width,
height,
yaw,
pitch,
distance,
planes_hash: hash_planes(planes),
}
}
#[must_use]
pub fn matches_key(&self, key: &GuideKey) -> bool {
self.key.as_ref() == Some(key)
}
pub fn adopt(&mut self, key: GuideKey, buffers: GuideBuffers) {
self.key = Some(key);
self.buffers = buffers;
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::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) as usize;
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], 1.0e6);
assert_eq!(guides.normal[i], Vec3::ZERO);
}
}
}
#[test]
fn guide_cache_reuses_buffers_when_the_key_is_unchanged() {
let planes = StandardGemCuts::standard_round_brilliant();
let mut cache = GuideCache::new();
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
assert_eq!(cache.generation(), 1);
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
assert_eq!(
cache.generation(),
1,
"an unchanged pose/geometry must reuse the cached guide buffers"
);
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
assert_eq!(cache.generation(), 1);
}
#[test]
fn guide_cache_regenerates_on_yaw_change() {
let planes = StandardGemCuts::standard_round_brilliant();
let mut cache = GuideCache::new();
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
cache.ensure(8, 8, 0.90, 0.45, 2.4, &planes);
assert_eq!(
cache.generation(),
2,
"a changed yaw must invalidate the cache"
);
}
#[test]
fn guide_cache_regenerates_on_pitch_or_distance_change() {
let planes = StandardGemCuts::standard_round_brilliant();
let mut cache = GuideCache::new();
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
cache.ensure(8, 8, 0.60, 0.80, 2.4, &planes);
assert_eq!(
cache.generation(),
2,
"a changed pitch must invalidate the cache"
);
cache.ensure(8, 8, 0.60, 0.80, 3.0, &planes);
assert_eq!(
cache.generation(),
3,
"a changed distance must invalidate the cache"
);
}
#[test]
fn guide_cache_regenerates_when_the_gem_geometry_changes() {
let srb = StandardGemCuts::standard_round_brilliant();
let emerald = StandardGemCuts::emerald_cut();
let mut cache = GuideCache::new();
cache.ensure(8, 8, 0.60, 0.45, 2.4, &srb);
cache.ensure(8, 8, 0.60, 0.45, 2.4, &emerald);
assert_eq!(
cache.generation(),
2,
"a changed cutting schedule must invalidate the cache even with an \
unchanged camera pose"
);
}
#[test]
fn guide_cache_regenerates_on_resolution_change() {
let planes = StandardGemCuts::standard_round_brilliant();
let mut cache = GuideCache::new();
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
cache.ensure(16, 8, 0.60, 0.45, 2.4, &planes);
assert_eq!(
cache.generation(),
2,
"a changed output resolution must invalidate the cache"
);
}
#[test]
fn ensure_signature_has_no_light_parameters() {
let planes = StandardGemCuts::standard_round_brilliant();
let mut cache = GuideCache::new();
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
assert_eq!(cache.generation(), 1);
}
#[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 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_cache_key_for_matches_what_ensure_uses_internally() {
let planes = StandardGemCuts::standard_round_brilliant();
let mut cache = GuideCache::new();
let key = GuideCache::key_for(8, 8, 0.60, 0.45, 2.4, &planes);
assert!(
!cache.matches_key(&key),
"a freshly-constructed cache must not match any key yet"
);
cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
assert!(
cache.matches_key(&key),
"the key ensure() just populated must equal key_for()'s independently \
computed key for the identical pose/geometry"
);
}
#[test]
fn guide_cache_adopt_installs_externally_computed_buffers_without_recomputing() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(0.60, 0.45, 2.4, 42.0);
let buffers = generate_guide_buffers(8, 8, &camera, &planes);
let key = GuideCache::key_for(8, 8, 0.60, 0.45, 2.4, &planes);
let mut cache = GuideCache::new();
cache.adopt(key.clone(), buffers.clone());
assert_eq!(
cache.generation(),
0,
"adopt() folds in an externally-computed result -- it must not be counted \
as this cache having run its own prepass"
);
assert!(cache.matches_key(&key));
let cached = cache.ensure(8, 8, 0.60, 0.45, 2.4, &planes);
assert_eq!(cached.depth, buffers.depth);
assert_eq!(cache.generation(), 0);
}
}