use crate::renderer::gpu::compute;
use super::{
FLOATS_PER_TUPLE, WORKGROUP_SIZE, renderer::GpuFrameRenderer, scene_buffers::TransportOutputs,
};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuReduceParams {
pub(crate) num_pixels: u32,
pub(crate) num_samples: u32,
pub(crate) _pad0: u32,
pub(crate) _pad1: u32,
}
pub struct PixelXyzOutput {
pub(super) buffer: wgpu::Buffer,
pub(super) capacity: usize,
}
impl PixelXyzOutput {
pub(crate) fn new(device: &wgpu::Device, capacity: usize) -> Self {
let usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC;
Self {
buffer: compute::zeroed_buffer::<f32>(
device,
"transport out pixel xyz (GPU-reduced)",
capacity * 3,
usage,
),
capacity,
}
}
}
pub struct StagingSlot {
pub(super) buffer: wgpu::Buffer,
pub(super) capacity: usize,
}
impl StagingSlot {
pub(crate) fn new(device: &wgpu::Device, label: &str, capacity: usize) -> Self {
let byte_len = (capacity * FLOATS_PER_TUPLE * size_of::<f32>()) as wgpu::BufferAddress;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: byte_len,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
Self { buffer, capacity }
}
}
pub const fn staging_needs_growth(current_capacity: usize, required: usize) -> bool {
current_capacity < required
}
pub fn copy_to_existing_staging<T: bytemuck::Pod>(
device: &wgpu::Device,
queue: &wgpu::Queue,
source: &wgpu::Buffer,
staging: &wgpu::Buffer,
count: usize,
) -> wgpu::SubmissionIndex {
let byte_len = (count * size_of::<T>()) as wgpu::BufferAddress;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("indicatrix gpu pipelined readback copy encoder (persistent staging)"),
});
encoder.copy_buffer_to_buffer(source, 0, staging, 0, byte_len);
queue.submit(Some(encoder.finish()))
}
impl GpuFrameRenderer {
pub(super) fn dispatch_reduce(
&self,
num_samples: u32,
staging_slot: usize,
pixels_this_chunk: usize,
outputs: &TransportOutputs,
pixel_output: &PixelXyzOutput,
) {
let reduce_params_buf = self.reduce_params_buffers[staging_slot]
.as_ref()
.expect("ensure_reduce_params_buffers just populated both slots");
let reduce_params = GpuReduceParams {
num_pixels: pixels_this_chunk as u32,
num_samples,
_pad0: 0,
_pad1: 0,
};
self.ctx
.queue
.write_buffer(reduce_params_buf, 0, bytemuck::bytes_of(&reduce_params));
let reduce_bind_group = compute::bind_buffers(
&self.ctx.device,
"reduce xyz bind group",
&self.reduce_pipeline,
&[
(0, reduce_params_buf),
(1, outputs.xyz()),
(2, &pixel_output.buffer),
],
);
let reduce_workgroups = (pixels_this_chunk as u32).div_ceil(WORKGROUP_SIZE as u32);
let _ = compute::dispatch(
&self.ctx.device,
&self.ctx.queue,
&self.reduce_pipeline,
&reduce_bind_group,
(reduce_workgroups, 1, 1),
);
}
pub(super) fn drain_pending_chunk(
&mut self,
chunk: super::PendingChunk,
spp: u32,
accum: Option<&mut [glam::Vec3]>,
) -> Result<(), super::GpuFrameError> {
let staging_buffer = &self.staging[chunk.staging_slot]
.as_ref()
.expect("dispatch_chunk populated this staging slot")
.buffer;
compute::finish_map_read_into(
&self.ctx.device,
staging_buffer,
chunk.copy_index,
&chunk.rx,
&mut self.xyz_scratch,
)
.map_err(|e| super::GpuFrameError::DeviceLost(e.to_string()))?;
let tuples_measured = chunk.pixels_this_chunk * spp as usize;
if tuples_measured > 0 {
let elapsed_ns = chunk.submitted_at.elapsed().as_nanos() as f64;
let measured_ns_per_tuple = elapsed_ns / tuples_measured as f64;
self.ns_per_tuple_ema =
Some(self.ns_per_tuple_ema.map_or(measured_ns_per_tuple, |prev| {
super::CHUNK_TIMING_EMA_ALPHA.mul_add(measured_ns_per_tuple - prev, prev)
}));
}
if let Some(accum) = accum {
let xyz = &self.xyz_scratch;
for local_pixel in 0..chunk.pixels_this_chunk {
let base = local_pixel * 3;
accum[chunk.first_pixel + local_pixel] +=
glam::Vec3::new(xyz[base], xyz[base + 1], xyz[base + 2]);
}
}
Ok(())
}
}