use std::time::Instant;
use crate::{
optics::raytracer::{EnvironmentSource, head_shadow_cosines, illuminant_temperature_k},
renderer::{
buffers::{GpuTransportParams, GpuWavefrontParams, transport_env_mode},
gpu::compute,
},
};
use super::{
ChunkFrameState, ChunkSlotResources, FIRST_DISPATCH_MAX_TUPLES, FLOATS_PER_TUPLE,
GpuPipelineKind, MAX_WORKGROUPS_PER_DIMENSION, MIN_CHUNK_BYTES, PendingChunk, TARGET_CHUNK_MS,
WORKGROUP_SIZE,
bind_groups::{
TransportDispatchArgs, WavefrontRoundBindGroups, build_bind_group, build_chunk_bind_group,
build_wavefront_round_bind_groups, exclusive_prefix_sum, wavefront_generate_bindings,
},
readback::copy_to_existing_staging,
renderer::GpuFrameRenderer,
scene_buffers::{FrameSceneBuffers, TransportOutputs, WavefrontRayBuffers},
};
pub(super) fn count_u32(count: usize) -> u32 {
u32::try_from(count).expect("a chunk's pixel and tuple counts fit u32")
}
pub(super) fn workgroups_for_u32(count: u32) -> u32 {
count.div_ceil(count_u32(WORKGROUP_SIZE))
}
pub(super) fn workgroups_for(count: usize) -> u32 {
workgroups_for_u32(count_u32(count))
}
pub fn encode_and_dispatch(args: &TransportDispatchArgs<'_>, total_tuples: usize) {
assert!(
total_tuples <= args.outputs.capacity,
"dispatch of {total_tuples} tuples exceeds output capacity {}",
args.outputs.capacity
);
let bind_group = build_bind_group(args);
let workgroups = count_u32(total_tuples).div_ceil(64);
compute::dispatch_and_wait(
&args.ctx.device,
&args.ctx.queue,
args.pipeline,
&bind_group,
(workgroups, 1, 1),
);
}
fn build_chunk_params(
state: &ChunkFrameState<'_>,
first_pixel: usize,
pixels_this_chunk: usize,
) -> GpuTransportParams {
GpuTransportParams::new(
count_u32(pixels_this_chunk),
state.scene.max_bounces,
state.sample_offset,
state.env_mode,
0.0,
state.temp_k,
state.spot_mult,
state.exposure,
state.light_yaw,
state.light_pitch,
state.white_balance.to_array(),
)
.with_pixel_offset(count_u32(first_pixel))
.with_debug_buffers_disabled()
.with_studio_use_d65(state.use_d65)
.with_studio_model(state.studio_model)
.with_tent_of(state.scene.environment)
.with_backdrop(state.backdrop)
.with_surface_glare(state.scene.environment.surface_glare())
.with_head_shadow(head_shadow_cosines(
state.scene.environment.head_shadow_deg(),
))
}
impl GpuFrameRenderer {
pub(super) fn dispatch_chunk(
&self,
state: &ChunkFrameState<'_>,
frame_buffers: &FrameSceneBuffers,
first_pixel: usize,
pixels_this_chunk: usize,
chunk_index: usize,
) -> PendingChunk {
let tuples = pixels_this_chunk * state.spp as usize;
let planes_len = state.scene.planes.len();
let params = build_chunk_params(state, first_pixel, pixels_this_chunk);
let staging_slot = chunk_index % 2;
let ChunkSlotResources {
outputs,
pixel_output,
staging_buffer,
params_buf,
} = self.resolve_chunk_slot_resources(staging_slot, tuples, pixels_this_chunk);
self.ctx
.queue
.write_buffer(params_buf, 0, bytemuck::bytes_of(¶ms));
let submitted_at = Instant::now();
match self.pipeline_kind {
GpuPipelineKind::Megakernel => {
let pipeline = self.pipeline_for_class(state.pipeline_class);
let bind_group = build_chunk_bind_group(
&self.ctx,
pipeline,
frame_buffers,
params_buf,
planes_len,
outputs,
);
let workgroups = workgroups_for(tuples);
let _ = compute::dispatch(
&self.ctx.device,
&self.ctx.queue,
pipeline,
&bind_group,
(workgroups, 1, 1),
);
}
GpuPipelineKind::Wavefront => {
self.dispatch_chunk_wavefront(
state,
frame_buffers,
params_buf,
planes_len,
outputs,
tuples,
);
}
}
self.dispatch_reduce(
state.spp,
staging_slot,
pixels_this_chunk,
outputs,
pixel_output,
);
let copy_index = copy_to_existing_staging::<f32>(
&self.ctx.device,
&self.ctx.queue,
&pixel_output.buffer,
staging_buffer,
pixels_this_chunk * 3,
);
let rx = compute::begin_map_read(staging_buffer);
PendingChunk {
staging_slot,
copy_index,
rx,
first_pixel,
pixels_this_chunk,
submitted_at,
}
}
fn dispatch_chunk_wavefront(
&self,
state: &ChunkFrameState<'_>,
frame_buffers: &FrameSceneBuffers,
params_buf: &wgpu::Buffer,
planes_len: usize,
outputs: &TransportOutputs,
tuples: usize,
) {
let pipelines = self
.wavefront_pipelines
.as_ref()
.expect("set_pipeline_kind(Wavefront) must be called before dispatching it");
let device = &self.ctx.device;
let queue = &self.ctx.queue;
let rays = WavefrontRayBuffers::new(device, tuples);
let wf_params_buf = compute::upload(
device,
"wavefront params",
&[GpuWavefrontParams {
chunk_rays: count_u32(tuples),
active_count: count_u32(tuples),
bounce: 0,
workgroup_count: workgroups_for(tuples),
}],
wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
let generate_bind_group = compute::bind_buffers(
device,
"wavefront generate bind group",
&pipelines.generate,
&wavefront_generate_bindings(&frame_buffers.camera, params_buf, &wf_params_buf, &rays),
);
let generate_workgroups = workgroups_for(tuples);
let _ = compute::dispatch(
device,
queue,
&pipelines.generate,
&generate_bind_group,
(generate_workgroups, 1, 1),
);
let bind_groups = build_wavefront_round_bind_groups(
device,
pipelines,
frame_buffers,
params_buf,
planes_len,
outputs,
&wf_params_buf,
&rays,
);
let (active_count, bounce) = self.run_wavefront_bounce_rounds(
state.scene.max_bounces,
count_u32(tuples),
&rays,
&wf_params_buf,
&bind_groups,
);
if active_count > 0 {
queue.write_buffer(
&wf_params_buf,
0,
bytemuck::bytes_of(&GpuWavefrontParams {
chunk_rays: count_u32(tuples),
active_count,
bounce,
workgroup_count: workgroups_for_u32(active_count),
}),
);
let finalize_workgroups = workgroups_for_u32(active_count);
let _ = compute::dispatch(
device,
queue,
&pipelines.finalize_survivors,
&bind_groups.finalize,
(finalize_workgroups, 1, 1),
);
}
}
fn run_wavefront_bounce_rounds(
&self,
max_bounces: u32,
chunk_rays: u32,
rays: &WavefrontRayBuffers,
wf_params_buf: &wgpu::Buffer,
bind_groups: &WavefrontRoundBindGroups,
) -> (u32, u32) {
let device = &self.ctx.device;
let queue = &self.ctx.queue;
let pipelines = self
.wavefront_pipelines
.as_ref()
.expect("set_pipeline_kind(Wavefront) must be called before dispatching it");
let mut active_count = chunk_rays;
let mut bounce = 0u32;
while active_count > 0 && bounce < max_bounces {
let workgroup_count = workgroups_for_u32(active_count);
queue.write_buffer(
wf_params_buf,
0,
bytemuck::bytes_of(&GpuWavefrontParams {
chunk_rays,
active_count,
bounce,
workgroup_count,
}),
);
compute::dispatch_and_wait(
device,
queue,
&pipelines.bounce,
&bind_groups.bounce,
(workgroup_count, 1, 1),
);
compute::dispatch_and_wait(
device,
queue,
&pipelines.compact_scan,
&bind_groups.compact_scan,
(workgroup_count, 1, 1),
);
let block_counts: Vec<u32> = compute::readback(
device,
queue,
&rays.block_alive_count,
workgroup_count as usize,
);
let (block_offsets, new_active_count) = exclusive_prefix_sum(&block_counts);
queue.write_buffer(&rays.block_offset, 0, bytemuck::cast_slice(&block_offsets));
compute::dispatch_and_wait(
device,
queue,
&pipelines.compact_scatter,
&bind_groups.compact_scatter,
(workgroup_count, 1, 1),
);
if new_active_count > 0 {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("wavefront active-list copy encoder"),
});
encoder.copy_buffer_to_buffer(
&rays.active_out,
0,
&rays.active_in,
0,
u64::from(new_active_count) * size_of::<u32>() as u64,
);
queue.submit(Some(encoder.finish()));
}
active_count = new_active_count;
bounce += 1;
}
(active_count, bounce)
}
pub(super) fn next_chunk_pixels(
ema_ns_per_tuple: Option<f64>,
byte_budget_pixels: usize,
spp: u32,
num_pixels: usize,
) -> usize {
let spp_usize = (spp as usize).max(1);
let Some(ns_per_tuple) = ema_ns_per_tuple.filter(|v| *v > 0.0) else {
let first_dispatch_pixels = (FIRST_DISPATCH_MAX_TUPLES / spp_usize).max(1);
return byte_budget_pixels
.min(first_dispatch_pixels)
.min(num_pixels)
.max(1);
};
let target_tuples = (TARGET_CHUNK_MS * 1.0e6 / ns_per_tuple).max(1.0);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "target_tuples is clamped to >= 1.0 and finite for a positive ns_per_tuple; `as` saturates and the result is clamped to the byte budget below"
)]
let target_pixels = ((target_tuples / spp_usize as f64).floor() as usize).max(1);
let min_pixels = chunk_pixels_for(MIN_CHUNK_BYTES, spp, num_pixels).min(byte_budget_pixels);
target_pixels.clamp(min_pixels, byte_budget_pixels)
}
}
pub fn chunk_pixels_for(budget_bytes: usize, spp: u32, num_pixels: usize) -> usize {
let budget_tuples = budget_bytes / (FLOATS_PER_TUPLE * size_of::<f32>());
let dispatch_limited_tuples = MAX_WORKGROUPS_PER_DIMENSION * WORKGROUP_SIZE;
let tuples_per_chunk = budget_tuples.min(dispatch_limited_tuples);
(tuples_per_chunk / spp as usize).max(1).min(num_pixels)
}
pub type EnvironmentParams = (u32, f32, f32, f32, f32, f32, bool, u32, f32);
pub const fn environment_params(environment: EnvironmentSource<'_>) -> EnvironmentParams {
match environment {
EnvironmentSource::Studio {
preset,
exposure,
light_yaw,
light_pitch,
backdrop,
..
} => (
transport_env_mode::STUDIO_RIG,
illuminant_temperature_k(preset),
preset.params().spot_mult,
exposure,
light_yaw,
light_pitch,
preset.uses_d65(),
preset.model().gpu_id(),
backdrop,
),
EnvironmentSource::HdrMap(_) => (
transport_env_mode::HDR_MAP,
0.0,
0.0,
0.0,
0.0,
0.0,
false,
0,
0.0,
),
}
}