use std::sync::atomic::AtomicBool;
use glam::Vec3;
use crate::{
geometry::GpuFacetPlane,
optics::{
materials::GemMaterial,
raytracer::{Camera, EnvironmentSource, FacetFinish},
},
};
#[cfg(feature = "gpu")]
use super::gpu::GpuFrameError;
#[cfg(feature = "gpu")]
use super::gpu::frame::{ChunkCursor, ChunkTurnOutcome, TurnRequest, classify_material};
#[cfg_attr(not(feature = "gpu"), allow(dead_code))]
pub struct GpuSceneRef<'a> {
pub camera: &'a Camera,
pub width: u32,
pub height: u32,
pub planes: &'a [GpuFacetPlane],
pub facet_finishes: &'a [FacetFinish],
pub material: &'a GemMaterial,
pub max_bounces: u32,
pub environment: EnvironmentSource<'a>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuAccumulate {
Done,
Declined,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GpuPipelineKind {
#[default]
Megakernel,
Wavefront,
}
#[cfg(feature = "gpu")]
const CHUNKS_PER_TURN: usize = 2;
#[cfg(feature = "gpu")]
struct Turnstile {
next_ticket: std::sync::atomic::AtomicU64,
now_serving: std::sync::Mutex<u64>,
turn_taken: std::sync::Condvar,
}
#[cfg(feature = "gpu")]
impl Turnstile {
const fn new() -> Self {
Self {
next_ticket: std::sync::atomic::AtomicU64::new(0),
now_serving: std::sync::Mutex::new(0),
turn_taken: std::sync::Condvar::new(),
}
}
fn take_ticket(&self) -> u64 {
self.next_ticket
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
fn wait_for_turn(&self, ticket: u64) -> TurnstileTurn<'_> {
let mut serving = self
.now_serving
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while *serving != ticket {
serving = self
.turn_taken
.wait(serving)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(serving);
TurnstileTurn { turnstile: self }
}
}
#[cfg(feature = "gpu")]
struct TurnstileTurn<'a> {
turnstile: &'a Turnstile,
}
#[cfg(feature = "gpu")]
impl Drop for TurnstileTurn<'_> {
fn drop(&mut self) {
let mut serving = self
.turnstile
.now_serving
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*serving += 1;
drop(serving);
self.turnstile.turn_taken.notify_all();
}
}
#[cfg(feature = "gpu")]
pub struct GpuBackend {
renderer: Option<std::sync::Mutex<super::gpu::GpuFrameRenderer>>,
turnstile: Turnstile,
lost: AtomicBool,
}
#[cfg(feature = "gpu")]
impl GpuBackend {
#[must_use]
pub fn acquire() -> Self {
let renderer = match super::gpu::GpuFrameRenderer::new() {
Ok(r) => {
tracing::info!(adapter = r.adapter_label(), "GPU render backend active");
Some(std::sync::Mutex::new(r))
}
Err(e) => {
tracing::info!("GPU render backend unavailable, using CPU tracer: {e}");
None
}
};
Self {
renderer,
turnstile: Turnstile::new(),
lost: AtomicBool::new(false),
}
}
#[must_use]
pub const fn disabled() -> Self {
Self {
renderer: None,
turnstile: Turnstile::new(),
lost: AtomicBool::new(false),
}
}
#[must_use]
pub fn adapter_label(&self) -> Option<String> {
if self.lost.load(std::sync::atomic::Ordering::Relaxed) {
return None;
}
self.renderer.as_ref().map(|m| {
m.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.adapter_label()
.to_string()
})
}
pub fn set_pipeline_kind(&self, kind: GpuPipelineKind) {
if let Some(mutex) = &self.renderer {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.set_pipeline_kind(kind);
}
}
pub fn try_accumulate(
&self,
scene: &GpuSceneRef<'_>,
sample_offset: u32,
spp: u32,
accum: &mut [Vec3],
) -> bool {
let never_cancel = AtomicBool::new(false);
matches!(
self.try_accumulate_cancellable(scene, sample_offset, spp, accum, &never_cancel),
GpuAccumulate::Done
)
}
pub fn try_accumulate_cancellable(
&self,
scene: &GpuSceneRef<'_>,
sample_offset: u32,
spp: u32,
accum: &mut [Vec3],
cancel: &AtomicBool,
) -> GpuAccumulate {
if self.lost.load(std::sync::atomic::Ordering::Relaxed) {
return GpuAccumulate::Declined;
}
let Some(mutex) = &self.renderer else {
return GpuAccumulate::Declined;
};
let gpu_scene = super::gpu::GpuFrameScene {
camera: scene.camera,
width: scene.width,
height: scene.height,
planes: scene.planes,
facet_finishes: scene.facet_finishes,
material: scene.material,
max_bounces: scene.max_bounces,
environment: scene.environment,
};
let pipeline_class = classify_material(gpu_scene.material);
let mut cursor = ChunkCursor::default();
let request = TurnRequest {
scene: &gpu_scene,
pipeline_class,
sample_offset,
spp,
cancel: Some(cancel),
max_chunks: CHUNKS_PER_TURN,
};
loop {
let ticket = self.turnstile.take_ticket();
let _turn = self.turnstile.wait_for_turn(ticket);
let mut renderer = mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let outcome = renderer.accumulate_turn(&request, accum, &mut cursor);
drop(renderer);
match outcome {
Ok(ChunkTurnOutcome::Done) => return GpuAccumulate::Done,
Ok(ChunkTurnOutcome::Cancelled) => return GpuAccumulate::Cancelled,
Ok(ChunkTurnOutcome::MoreWork) => {}
Err(GpuFrameError::DeviceLost(why)) => {
tracing::warn!(
"GPU device lost ({why}), permanently disabling the GPU backend for \
the rest of this process -- every later call falls back to the CPU \
tracer"
);
self.lost.store(true, std::sync::atomic::Ordering::Relaxed);
return GpuAccumulate::Declined;
}
Err(e) => {
tracing::debug!("GPU declined this dispatch, using CPU tracer: {e}");
return GpuAccumulate::Declined;
}
}
}
}
}
#[cfg(not(feature = "gpu"))]
pub struct GpuBackend;
#[cfg(not(feature = "gpu"))]
impl GpuBackend {
#[must_use]
pub const fn acquire() -> Self {
Self
}
#[must_use]
pub const fn disabled() -> Self {
Self
}
#[must_use]
pub const fn adapter_label(&self) -> Option<String> {
None
}
pub const fn set_pipeline_kind(&self, _kind: GpuPipelineKind) {}
#[allow(
clippy::unused_self,
reason = "signature must match the `gpu`-gated GpuBackend::try_accumulate"
)]
pub const fn try_accumulate(
&self,
_scene: &GpuSceneRef<'_>,
_sample_offset: u32,
_spp: u32,
_accum: &mut [Vec3],
) -> bool {
false
}
pub const fn try_accumulate_cancellable(
&self,
_scene: &GpuSceneRef<'_>,
_sample_offset: u32,
_spp: u32,
_accum: &mut [Vec3],
_cancel: &AtomicBool,
) -> GpuAccumulate {
GpuAccumulate::Declined
}
}
#[cfg(all(test, feature = "gpu"))]
mod tests {
use super::Turnstile;
use std::{sync::Mutex, thread};
#[test]
fn turnstile_serves_tickets_in_order_even_when_threads_queue_out_of_order() {
let turnstile = Turnstile::new();
let served_order: Mutex<Vec<u64>> = Mutex::new(Vec::new());
let t0 = turnstile.take_ticket();
let t1 = turnstile.take_ticket();
let t2 = turnstile.take_ticket();
thread::scope(|scope| {
scope.spawn(|| {
let _turn = turnstile.wait_for_turn(t2);
served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(2);
});
scope.spawn(|| {
let _turn = turnstile.wait_for_turn(t1);
served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(1);
});
{
let _turn0 = turnstile.wait_for_turn(t0);
served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(0);
}
});
assert_eq!(
*served_order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec![0, 1, 2],
"tickets must be served in the order they were taken, not the order threads \
happened to start waiting"
);
}
#[test]
fn turnstile_releases_on_an_early_return_out_of_the_turn_scope() {
fn takes_a_turn_then_bails_early(turnstile: &Turnstile, ticket: u64) -> Option<()> {
let _turn = turnstile.wait_for_turn(ticket);
None }
let turnstile = Turnstile::new();
let t0 = turnstile.take_ticket();
let t1 = turnstile.take_ticket();
assert_eq!(takes_a_turn_then_bails_early(&turnstile, t0), None);
let _turn1 = turnstile.wait_for_turn(t1);
}
}