#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
use std::sync::atomic::Ordering;
use glam::Vec3;
use super::{
ChunkCursor, ChunkFrameState, GpuFrameError, GpuPipelineKind, PendingChunk, TurnRequest,
TurnSetup, dispatch::chunk_pixels_for, renderer::GpuFrameRenderer,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BatchEvent {
Done(usize),
Declined(usize),
Cancelled(usize),
}
#[derive(Debug, Default)]
pub struct BatchStep {
pub(crate) events: Vec<BatchEvent>,
pub(crate) dispatched: bool,
}
struct BatchPending {
chunk: PendingChunk,
item: usize,
spp: u32,
last_of_item: bool,
}
struct BatchSetup<'a> {
item: usize,
state: ChunkFrameState<'a>,
byte_budget_pixels: usize,
}
pub struct BatchState<'a> {
next_item: usize,
cursor: ChunkCursor,
global_chunk: usize,
pending: Option<BatchPending>,
setup: Option<BatchSetup<'a>>,
sized_for: GpuPipelineKind,
finished: bool,
}
impl BatchState<'_> {
pub(crate) fn new() -> Self {
Self {
next_item: 0,
cursor: ChunkCursor::default(),
global_chunk: 0,
pending: None,
setup: None,
sized_for: GpuPipelineKind::default(),
finished: false,
}
}
pub(crate) const fn next_item(&self) -> usize {
self.next_item
}
pub(crate) const fn is_finished(&self) -> bool {
self.finished
}
}
impl GpuFrameRenderer {
pub(crate) fn batch_begin_turn<'a>(
&mut self,
requests: &[TurnRequest<'a>],
state: &mut BatchState<'a>,
) {
debug_assert!(
state.pending.is_none(),
"a turn must start with nothing in flight"
);
state.setup = None;
self.batch_reserve(requests);
state.sized_for = self.pipeline_kind;
}
fn batch_reserve(&mut self, requests: &[TurnRequest<'_>]) {
let mut max_tuples = 0usize;
let mut max_pixels = 0usize;
for request in requests {
let num_pixels = request.scene.width as usize * request.scene.height as usize;
if request.spp == 0 || num_pixels == 0 || !request.scene.material.gpu_supported() {
continue;
}
let budget = chunk_pixels_for(self.chunk_budget_bytes, request.spp, num_pixels);
let budget = self.cap_byte_budget_for_wavefront(budget, request.spp);
max_pixels = max_pixels.max(budget);
max_tuples = max_tuples.max(budget * request.spp as usize);
}
if max_pixels == 0 {
return;
}
self.ensure_capacity(max_tuples);
self.ensure_pixel_capacity(max_pixels);
self.ensure_staging_capacity(max_pixels);
self.ensure_params_buffers();
self.ensure_reduce_params_buffers();
}
pub(crate) fn batch_step<'a>(
&mut self,
requests: &[TurnRequest<'a>],
scratches: &mut [Vec<Vec3>],
state: &mut BatchState<'a>,
) -> Result<BatchStep, GpuFrameError> {
if self.poisoned {
return Err(GpuFrameError::DeviceLost(
"GPU renderer poisoned by a previous failed chunk readback".to_string(),
));
}
if self.turn_in_progress {
self.abandon_in_flight();
return Err(GpuFrameError::DeviceLost(
"GPU renderer poisoned: a previous turn unwound without completing".to_string(),
));
}
self.turn_in_progress = true;
let result = self.batch_step_body(requests, scratches, state);
self.turn_in_progress = false;
result
}
pub(crate) fn batch_yield(
&mut self,
scratches: &mut [Vec<Vec3>],
state: &mut BatchState<'_>,
) -> Result<Vec<BatchEvent>, GpuFrameError> {
if self.poisoned {
return Err(GpuFrameError::DeviceLost(
"GPU renderer poisoned by a previous failed chunk readback".to_string(),
));
}
let mut step = BatchStep::default();
self.drain_batch_pending(state, scratches, true, &mut step)?;
state.setup = None;
self.check_uncaptured_error()?;
Ok(step.events)
}
fn batch_step_body<'a>(
&mut self,
requests: &[TurnRequest<'a>],
scratches: &mut [Vec<Vec3>],
state: &mut BatchState<'a>,
) -> Result<BatchStep, GpuFrameError> {
let mut step = BatchStep::default();
if state.finished {
return Ok(step);
}
let Some(request) = requests.get(state.next_item) else {
self.drain_batch_pending(state, scratches, true, &mut step)?;
state.finished = true;
self.check_uncaptured_error()?;
return Ok(step);
};
let num_pixels = request.scene.width as usize * request.scene.height as usize;
let trivial = request.spp == 0 || num_pixels == 0;
if trivial || !request.scene.material.gpu_supported() {
if state.pending.is_some() {
self.drain_batch_pending(state, scratches, true, &mut step)?;
self.check_uncaptured_error()?;
return Ok(step);
}
step.events.push(if trivial {
BatchEvent::Done(state.next_item)
} else {
BatchEvent::Declined(state.next_item)
});
state.next_item += 1;
state.cursor = ChunkCursor::default();
return Ok(step);
}
if request
.cancel
.is_some_and(|flag| flag.load(Ordering::Relaxed))
{
let keep = state
.pending
.as_ref()
.is_some_and(|pending| pending.last_of_item);
self.drain_batch_pending(state, scratches, keep, &mut step)?;
step.events.push(BatchEvent::Cancelled(state.next_item));
state.finished = true;
self.check_uncaptured_error()?;
return Ok(step);
}
if self.pipeline_kind != state.sized_for {
self.drain_batch_pending(state, scratches, true, &mut step)?;
state.setup = None;
self.batch_reserve(requests);
state.sized_for = self.pipeline_kind;
}
if !self.ensure_batch_setup(request, state, scratches, &mut step)? {
return Ok(step);
}
let setup = state
.setup
.as_ref()
.expect("the setup above is populated for this picture");
let first_pixel = state.cursor.first_pixel;
let chunk_pixels = Self::next_chunk_pixels(
self.ns_per_tuple_ema,
setup.byte_budget_pixels,
request.spp,
num_pixels,
);
let pixels_this_chunk = chunk_pixels.min(num_pixels - first_pixel);
let frame_buffers = self
.scene_buffers
.as_ref()
.expect("prepare_turn stored the scene buffers");
let chunk = self.dispatch_chunk(
&setup.state,
frame_buffers,
first_pixel,
pixels_this_chunk,
state.global_chunk,
);
let next_first_pixel = first_pixel + pixels_this_chunk;
let last_of_item = next_first_pixel >= num_pixels;
let previous = state.pending.replace(BatchPending {
chunk,
item: state.next_item,
spp: request.spp,
last_of_item,
});
step.dispatched = true;
state.global_chunk += 1;
if last_of_item {
state.next_item += 1;
state.cursor = ChunkCursor::default();
state.setup = None;
} else {
state.cursor.first_pixel = next_first_pixel;
state.cursor.chunk_index += 1;
}
if let Some(previous) = previous {
self.drain_one(previous, scratches, true, &mut step)?;
}
self.check_uncaptured_error()?;
Ok(step)
}
fn ensure_batch_setup<'a>(
&mut self,
request: &TurnRequest<'a>,
state: &mut BatchState<'a>,
scratches: &mut [Vec<Vec3>],
step: &mut BatchStep,
) -> Result<bool, GpuFrameError> {
if state
.setup
.as_ref()
.is_some_and(|setup| setup.item == state.next_item)
{
return Ok(true);
}
match self.prepare_turn(request) {
Ok(TurnSetup {
state: chunk_state,
frame_buffers,
byte_budget_pixels,
}) => {
self.scene_buffers = Some(frame_buffers);
state.setup = Some(BatchSetup {
item: state.next_item,
state: chunk_state,
byte_budget_pixels,
});
Ok(true)
}
Err(GpuFrameError::DeviceLost(why)) => Err(GpuFrameError::DeviceLost(why)),
Err(error) => {
tracing::debug!("GPU declined a batch picture, using CPU tracer: {error}");
self.drain_batch_pending(state, scratches, true, step)?;
step.events.push(BatchEvent::Declined(state.next_item));
state.next_item += 1;
state.cursor = ChunkCursor::default();
state.setup = None;
self.check_uncaptured_error()?;
Ok(false)
}
}
}
fn drain_batch_pending(
&mut self,
state: &mut BatchState<'_>,
scratches: &mut [Vec<Vec3>],
sum: bool,
step: &mut BatchStep,
) -> Result<(), GpuFrameError> {
state.pending.take().map_or(Ok(()), |pending| {
self.drain_one(pending, scratches, sum, step)
})
}
fn drain_one(
&mut self,
pending: BatchPending,
scratches: &mut [Vec<Vec3>],
sum: bool,
step: &mut BatchStep,
) -> Result<(), GpuFrameError> {
let BatchPending {
chunk,
item,
spp,
last_of_item,
} = pending;
let accum = sum.then(|| scratches[item].as_mut_slice());
if let Err(error) = self.drain_pending_chunk(chunk, spp, accum) {
self.abandon_in_flight();
return Err(error);
}
if sum && last_of_item {
step.events.push(BatchEvent::Done(item));
}
Ok(())
}
}