use super::{sample_cursor::SampleCursor, scene_snapshot::SceneSnapshot};
use glam::Vec3;
use indicatrix::{
optics::raytracer::{
Camera, EnvironmentSource, pixel_rotations, sample_draws, trace_spectral_ray_with_finish,
},
renderer::gpu_backend::{GpuBackend, GpuSceneRef},
};
use std::{
sync::{
Mutex,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
thread,
time::Duration,
};
pub(super) const HYBRID_MIN_SPP: u32 = 8;
const TARGET_BATCHES: u32 = 40;
pub(super) fn local_chunk_size(samples_per_pixel: u32) -> u32 {
(samples_per_pixel / TARGET_BATCHES).max(1)
}
const LOCAL_RETRY_POLL_INTERVAL: Duration = Duration::from_millis(200);
pub(super) struct ExportCtx<'a> {
pub(super) width: u32,
pub(super) height: u32,
pub(super) camera: &'a Camera,
pub(super) scene: &'a SceneSnapshot,
pub(super) gpu: &'a GpuBackend,
pub(super) gpu_scene: &'a GpuSceneRef<'a>,
}
pub(super) fn calibrate_split(
ctx: &ExportCtx<'_>,
samples_done: &mut u32,
accum: &mut [Vec3],
gpu_accum: &mut [Vec3],
cancel: &AtomicBool,
) -> Option<f64> {
if !ctx
.gpu
.try_accumulate(ctx.gpu_scene, *samples_done, 1, gpu_accum)
{
return None;
}
*samples_done += 1;
let start = std::time::Instant::now();
if !ctx
.gpu
.try_accumulate(ctx.gpu_scene, *samples_done, 1, gpu_accum)
{
return None;
}
let gpu_time = start.elapsed().as_secs_f64().max(1e-9);
*samples_done += 1;
if cancel.load(Ordering::Relaxed) {
return None;
}
let start = std::time::Instant::now();
render_batch(
ctx.width,
ctx.height,
1,
*samples_done,
ctx.camera,
ctx.scene,
accum,
);
let cpu_time = start.elapsed().as_secs_f64().max(1e-9);
*samples_done += 1;
let frac = cpu_time / (gpu_time + cpu_time);
Some(frac.clamp(0.0, 1.0))
}
pub(super) fn hybrid_batch(
ctx: &ExportCtx<'_>,
samples_done: u32,
this_batch: u32,
gpu_frac: &mut Option<f64>,
accum: &mut [Vec3],
gpu_accum: &mut [Vec3],
) {
let frac = gpu_frac.unwrap_or(0.0);
let gpu_share = (f64::from(this_batch) * frac).round() as u32;
let gpu_share = gpu_share.min(this_batch);
let cpu_share = this_batch - gpu_share;
if gpu_share == 0 {
render_batch(
ctx.width,
ctx.height,
this_batch,
samples_done,
ctx.camera,
ctx.scene,
accum,
);
return;
}
let (gpu_ok, gpu_elapsed, cpu_elapsed) = if cpu_share == 0 {
let start = std::time::Instant::now();
let ok = ctx
.gpu
.try_accumulate(ctx.gpu_scene, samples_done, gpu_share, gpu_accum);
(ok, start.elapsed(), std::time::Duration::ZERO)
} else {
thread::scope(|s| {
let gpu_task = s.spawn(|| {
let start = std::time::Instant::now();
let ok = ctx
.gpu
.try_accumulate(ctx.gpu_scene, samples_done, gpu_share, gpu_accum);
(ok, start.elapsed())
});
let cpu_start = std::time::Instant::now();
render_batch(
ctx.width,
ctx.height,
cpu_share,
samples_done + gpu_share,
ctx.camera,
ctx.scene,
accum,
);
let cpu_elapsed = cpu_start.elapsed();
let (gpu_ok, gpu_elapsed) = gpu_task
.join()
.unwrap_or((false, std::time::Duration::ZERO));
(gpu_ok, gpu_elapsed, cpu_elapsed)
})
};
if !gpu_ok {
render_batch(
ctx.width,
ctx.height,
gpu_share,
samples_done,
ctx.camera,
ctx.scene,
accum,
);
*gpu_frac = None;
return;
}
if cpu_share > 0 {
let gpu_rate = f64::from(gpu_share) / gpu_elapsed.as_secs_f64().max(1e-9);
let cpu_rate = f64::from(cpu_share) / cpu_elapsed.as_secs_f64().max(1e-9);
let measured_frac = gpu_rate / (gpu_rate + cpu_rate);
let updated = frac.mul_add(0.7, measured_frac * 0.3);
*gpu_frac = Some(updated.clamp(0.0, 1.0));
}
}
#[expect(
clippy::too_many_arguments,
reason = "every argument is a distinct piece of the local hybrid loop's own state \
(scene/GPU context, the shared cursor and its claim size, the CPU/GPU \
split estimate, the two accumulation buffers, remote-lane-done \
signalling, cancellation, and the progress callback) -- bundling them \
into a struct would just move the same count into field access, not \
reduce it"
)]
pub(super) fn run_local_batches(
ctx: &ExportCtx<'_>,
cursor: &SampleCursor,
local_chunk_size: u32,
remote_lane_done: &AtomicBool,
gpu_frac: &mut Option<f64>,
accum: &mut [Vec3],
gpu_accum: &mut [Vec3],
cancel: &AtomicBool,
mut on_batch: impl FnMut(u32, &[Vec3], &[Vec3]),
) -> bool {
let mut local_traced = 0u32;
loop {
if cancel.load(Ordering::Relaxed) {
return true;
}
let Some((start, count)) = cursor.claim_local(local_chunk_size) else {
if remote_lane_done.load(Ordering::Acquire) {
return false;
}
thread::sleep(LOCAL_RETRY_POLL_INTERVAL);
continue;
};
if gpu_frac.is_some() {
hybrid_batch(ctx, start, count, gpu_frac, accum, gpu_accum);
} else if !ctx
.gpu
.try_accumulate(ctx.gpu_scene, start, count, gpu_accum)
{
render_batch(
ctx.width, ctx.height, count, start, ctx.camera, ctx.scene, accum,
);
}
local_traced += count;
on_batch(local_traced, accum, gpu_accum);
}
}
pub fn render_batch(
width: u32,
height: u32,
batch_spp: u32,
samples_already_done: u32,
camera: &Camera,
scene: &SceneSnapshot,
accum: &mut [Vec3],
) {
let num_threads = thread::available_parallelism().map_or(8, std::num::NonZero::get);
let width_usize = width as usize;
let rows: Vec<Option<&mut [Vec3]>> = accum.chunks_mut(width_usize).map(Some).collect();
let rows = Mutex::new(rows);
let next_row = AtomicUsize::new(0);
let environment = scene.env_map.as_deref().map_or_else(
|| {
scene
.lighting_preset
.studio(scene.exposure, scene.light_yaw, scene.light_pitch)
},
EnvironmentSource::HdrMap,
);
thread::scope(|s| {
for _ in 0..num_threads {
let rows = &rows;
let next_row = &next_row;
s.spawn(move || {
loop {
let y = next_row.fetch_add(1, Ordering::Relaxed);
if y >= height as usize {
break;
}
let row = rows
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)[y]
.take()
.expect("each row index is claimed by exactly one thread via fetch_add");
for (x, pixel) in row.iter_mut().enumerate() {
let global_pixel_idx = (y * width_usize + x) as u32;
let mut sample_sum = Vec3::ZERO;
let rot = pixel_rotations(global_pixel_idx);
for s_idx in 0..batch_spp {
let sample_num = samples_already_done + s_idx;
let draws = sample_draws(global_pixel_idx, sample_num, &rot);
let ray = camera.generate_ray(
x as f32,
y as f32,
width as f32,
height as f32,
draws.jitter_x,
draws.jitter_y,
);
sample_sum += trace_spectral_ray_with_finish(
ray,
&scene.active_planes,
&scene.facet_finishes,
&scene.material,
scene.max_bounces,
environment,
draws.seed,
draws.hero_rand,
None,
);
}
*pixel += sample_sum;
}
}
});
}
});
}