use crate::bridge::render_thread::hash_planes;
use indicatrix::{
geometry::plane::GpuFacetPlane,
optics::raytracer::{Camera, DEFAULT_FOV_DEG},
};
use std::sync::atomic::AtomicBool;
pub use indicatrix::renderer::guide_pass::{
GuideBuffers, generate_guide_buffers_cancellable, generate_guide_buffers_into,
};
#[cfg(test)]
pub use indicatrix::renderer::guide_pass::generate_guide_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, DEFAULT_FOV_DEG);
let completed = generate_guide_buffers_into(
width,
height,
&camera,
planes,
&AtomicBool::new(false),
&mut self.buffers,
);
debug_assert!(completed, "an uncancelled prepass always completes");
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 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 set of cutting instructions 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 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, DEFAULT_FOV_DEG);
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);
}
}