use std::time::{Duration, Instant};
use glam::Vec3;
use crate::optics::raytracer::{
build_plane_soa, pixel_rotations, sample_draws, trace_spectral_ray_with_finish_soa,
};
use super::frame::{GpuFrameError, GpuFrameRenderer, GpuFrameScene};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HybridSplit {
pub gpu_samples: u32,
pub cpu_samples: u32,
}
impl HybridSplit {
#[must_use]
pub const fn total_spp(&self) -> u32 {
self.gpu_samples + self.cpu_samples
}
#[must_use]
pub const fn cpu_only(total_spp: u32) -> Self {
Self {
gpu_samples: 0,
cpu_samples: total_spp,
}
}
pub fn calibrated(
renderer: &mut GpuFrameRenderer,
scene: &GpuFrameScene<'_>,
total_spp: u32,
warmup_spp: u32,
) -> Result<Self, GpuFrameError> {
if !scene.material.gpu_supported() {
return Ok(Self::cpu_only(total_spp));
}
if total_spp == 0 || warmup_spp == 0 {
return Ok(Self {
gpu_samples: 0,
cpu_samples: total_spp,
});
}
let num_pixels = scene.width as usize * scene.height as usize;
let mut gpu_throwaway = vec![Vec3::ZERO; num_pixels];
renderer.accumulate(scene, 0, warmup_spp, &mut gpu_throwaway)?;
let mut gpu_warmup = vec![Vec3::ZERO; num_pixels];
let gpu_start = Instant::now();
renderer.accumulate(scene, 0, warmup_spp, &mut gpu_warmup)?;
let gpu_rate = samples_per_sec(num_pixels, warmup_spp, gpu_start.elapsed());
let cpu_start = Instant::now();
let _cpu_warmup = cpu_trace_range(scene, 0, warmup_spp);
let cpu_rate = samples_per_sec(num_pixels, warmup_spp, cpu_start.elapsed());
Ok(Self::from_rates(total_spp, gpu_rate, cpu_rate))
}
fn from_rates(total_spp: u32, gpu_rate: f64, cpu_rate: f64) -> Self {
let total_rate = gpu_rate + cpu_rate;
if total_rate <= 0.0 {
let gpu_samples = total_spp / 2;
return Self {
gpu_samples,
cpu_samples: total_spp - gpu_samples,
};
}
let gpu_share = gpu_rate / total_rate;
let gpu_samples = f64::from(total_spp)
.mul_add(gpu_share, 0.5)
.floor()
.min(f64::from(total_spp)) as u32;
Self {
gpu_samples,
cpu_samples: total_spp - gpu_samples,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct HybridStats {
pub wall_time: Duration,
pub gpu_samples: u32,
pub cpu_samples: u32,
pub gpu_samples_per_sec: f64,
pub cpu_samples_per_sec: f64,
pub forced_cpu_only: bool,
}
pub fn render_hybrid(
renderer: &mut GpuFrameRenderer,
scene: &GpuFrameScene<'_>,
requested_split: HybridSplit,
accum: &mut [Vec3],
) -> Result<HybridStats, GpuFrameError> {
let num_pixels = scene.width as usize * scene.height as usize;
assert_eq!(
accum.len(),
num_pixels,
"accumulation buffer must have one entry per pixel"
);
let forced_cpu_only = !scene.material.gpu_supported();
let split = if forced_cpu_only {
HybridSplit::cpu_only(requested_split.total_spp())
} else {
requested_split
};
let wall_start = Instant::now();
let (gpu_result, cpu_buf, cpu_elapsed) = std::thread::scope(|s| {
let gpu_handle = s.spawn(move || -> Result<(Vec<Vec3>, Duration), GpuFrameError> {
let mut buf = vec![Vec3::ZERO; num_pixels];
if split.gpu_samples == 0 {
return Ok((buf, Duration::ZERO));
}
let start = Instant::now();
renderer.accumulate(scene, 0, split.gpu_samples, &mut buf)?;
Ok((buf, start.elapsed()))
});
let cpu_start = Instant::now();
let cpu_buf = if split.cpu_samples == 0 {
vec![Vec3::ZERO; num_pixels]
} else {
cpu_trace_range(scene, split.gpu_samples, split.cpu_samples)
};
let cpu_elapsed = cpu_start.elapsed();
let gpu_result = gpu_handle
.join()
.expect("hybrid GPU dispatch thread panicked");
(gpu_result, cpu_buf, cpu_elapsed)
});
let (gpu_buf, gpu_elapsed) = gpu_result?;
for ((dst, gpu_sample), cpu_sample) in accum.iter_mut().zip(gpu_buf).zip(cpu_buf) {
*dst += gpu_sample + cpu_sample;
}
Ok(HybridStats {
wall_time: wall_start.elapsed(),
gpu_samples: split.gpu_samples,
cpu_samples: split.cpu_samples,
gpu_samples_per_sec: samples_per_sec(num_pixels, split.gpu_samples, gpu_elapsed),
cpu_samples_per_sec: samples_per_sec(num_pixels, split.cpu_samples, cpu_elapsed),
forced_cpu_only,
})
}
fn samples_per_sec(num_pixels: usize, samples: u32, elapsed: Duration) -> f64 {
if samples == 0 {
return 0.0;
}
let total_samples = num_pixels as f64 * f64::from(samples);
total_samples / elapsed.as_secs_f64().max(f64::EPSILON)
}
pub fn cpu_accumulate(scene: &GpuFrameScene<'_>, sample_offset: u32, spp: u32, accum: &mut [Vec3]) {
let num_pixels = scene.width as usize * scene.height as usize;
assert_eq!(
accum.len(),
num_pixels,
"accumulation buffer must have one entry per pixel"
);
let sums = cpu_trace_range(scene, sample_offset, spp);
for (dst, src) in accum.iter_mut().zip(sums) {
*dst += src;
}
}
fn cpu_sample_xyz(
scene: &GpuFrameScene<'_>,
plane_soa: &crate::simd::PlanesSoA32,
pixel: u32,
sample_num: u32,
) -> Vec3 {
let width = scene.width;
let x = pixel % width;
let y = pixel / width;
let rot = pixel_rotations(pixel);
let draws = sample_draws(pixel, sample_num, &rot);
let ray = scene.camera.generate_ray(
x as f32,
y as f32,
width as f32,
scene.height as f32,
draws.jitter_x,
draws.jitter_y,
);
trace_spectral_ray_with_finish_soa(
ray,
scene.planes,
plane_soa,
scene.facet_finishes,
scene.material,
scene.max_bounces,
scene.environment,
draws.seed,
draws.hero_rand,
None,
)
}
fn cpu_trace_range(scene: &GpuFrameScene<'_>, sample_offset: u32, spp: u32) -> Vec<Vec3> {
let num_pixels = scene.width as usize * scene.height as usize;
let mut out = vec![Vec3::ZERO; num_pixels];
if spp == 0 || num_pixels == 0 {
return out;
}
let num_threads = std::thread::available_parallelism()
.map_or(4, std::num::NonZero::get)
.min(num_pixels);
let plane_soa = build_plane_soa(scene.planes);
let partials: Vec<Vec<(usize, Vec3)>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..num_threads)
.map(|thread_idx| {
let plane_soa = &plane_soa;
s.spawn(move || {
let mut local = Vec::with_capacity(num_pixels / num_threads + 1);
let mut pixel = thread_idx;
while pixel < num_pixels {
let mut sum = Vec3::ZERO;
for local_sample in 0..spp {
let sample_num = sample_offset + local_sample;
sum += cpu_sample_xyz(scene, plane_soa, pixel as u32, sample_num);
}
local.push((pixel, sum));
pixel += num_threads;
}
local
})
})
.collect();
let mut partials = Vec::with_capacity(handles.len());
for handle in handles {
partials.push(
handle
.join()
.expect("hybrid CPU trace worker thread panicked"),
);
}
partials
});
for part in partials {
for (pixel, sum) in part {
out[pixel] = sum;
}
}
out
}