use glam::Vec3;
use wgpu::BufferUsages;
use crate::{
optics::raytracer::{EnvironmentSource, environment::environment_white_balance},
renderer::{
buffers::{GpuCameraParams, GpuGemMaterial, GpuTransportParams, encode_facet_finishes},
env_map_gpu::HdrEnvGpuData,
gpu::{GpuContext, MEGAKERNEL_STORAGE_BUFFERS, compute},
},
};
use super::{
CHUNK_BUDGET_BYTES, GpuFrameError, GpuFrameScene, GpuPipelineKind, REDUCE_SHADER_SRC,
WORKGROUP_SIZE,
bind_groups::{TransportDispatchArgs, build_bind_group},
classify_material,
dispatch::{EnvironmentParams, chunk_pixels_for, environment_params},
readback::GpuReduceParams,
renderer::GpuFrameRenderer,
};
#[allow(
clippy::future_not_send,
reason = "wasm32-unknown-unknown has no second thread to send a future to; see the \
module-level comment above this impl block"
)]
impl GpuFrameRenderer {
pub async fn new_async() -> Result<Self, GpuFrameError> {
let ctx = GpuContext::acquire_async()
.await
.map_err(GpuFrameError::Acquire)?;
let available = ctx.device.limits().max_storage_buffers_per_shader_stage;
if available < MEGAKERNEL_STORAGE_BUFFERS {
return Err(GpuFrameError::DeviceLimits {
needed: MEGAKERNEL_STORAGE_BUFFERS,
available,
});
}
let info = ctx.adapter.get_info();
let adapter_label = format!("{} ({:?})", info.name, info.backend);
let pipeline_cache = compute::create_pipeline_cache(&ctx.device);
let reduce_pipeline = compute::create_compute_pipeline_cached(
&ctx.device,
pipeline_cache.as_ref(),
"reduce_xyz_main",
REDUCE_SHADER_SRC,
"reduce_xyz_main",
&[],
);
Ok(Self {
ctx,
pipeline_generic: std::sync::OnceLock::new(),
pipeline_cache,
pipeline_isotropic: None,
pipeline_uniaxial: None,
pipeline_biaxial: None,
reduce_pipeline,
adapter_label,
outputs: [None, None],
pixel_outputs: [None, None],
staging: [None, None],
params_buffers: [None, None],
reduce_params_buffers: [None, None],
scene_buffers: None,
xyz_scratch: Vec::new(),
chunk_budget_bytes: CHUNK_BUDGET_BYTES,
ns_per_tuple_ema: None,
pipeline_kind: GpuPipelineKind::default(),
wavefront_pipelines: None,
poisoned: false,
turn_in_progress: false,
})
}
pub async fn accumulate_async(
&mut self,
scene: &GpuFrameScene<'_>,
sample_offset: u32,
spp: u32,
accum: &mut [Vec3],
) -> Result<(), GpuFrameError> {
let pipeline_class = classify_material(scene.material);
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"
);
if spp == 0 || num_pixels == 0 {
return Ok(());
}
if !scene.material.gpu_supported() {
return Err(GpuFrameError::UnsupportedMaterial(
scene.material.name.clone(),
));
}
let call = AsyncCallInputs::new(self, scene, pipeline_class, spp, sample_offset)?;
self.ensure_specialized_pipeline(pipeline_class);
let chunk_pixels = chunk_pixels_for(self.chunk_budget_bytes, spp, num_pixels);
self.ensure_capacity(chunk_pixels * spp as usize);
self.ensure_pixel_capacity(chunk_pixels);
let mut first_pixel = 0usize;
let mut chunk_index = 0usize;
while first_pixel < num_pixels {
let pixels_this_chunk = chunk_pixels.min(num_pixels - first_pixel);
let staging =
self.submit_chunk_async(&call, chunk_index, first_pixel, pixels_this_chunk);
let xyz: Vec<f32> = map_read_async(&staging, pixels_this_chunk * 3).await?;
for local_pixel in 0..pixels_this_chunk {
let base = local_pixel * 3;
accum[first_pixel + local_pixel] +=
Vec3::new(xyz[base], xyz[base + 1], xyz[base + 2]);
}
first_pixel += pixels_this_chunk;
chunk_index += 1;
}
Ok(())
}
fn submit_chunk_async(
&self,
call: &AsyncCallInputs<'_, '_>,
chunk_index: usize,
first_pixel: usize,
pixels_this_chunk: usize,
) -> wgpu::Buffer {
let spp = call.spp;
let tuples = pixels_this_chunk * spp as usize;
let (camera_params, params) = call.chunk_params(first_pixel, pixels_this_chunk);
let outputs = self.outputs[chunk_index % 2]
.as_ref()
.expect("ensure_capacity just populated both slots");
let bind_args = TransportDispatchArgs {
ctx: &self.ctx,
pipeline: self.pipeline_for_class(call.pipeline_class),
camera_params: &camera_params,
params: ¶ms,
material: &call.gpu_material,
planes: call.scene.planes,
facet_finishes: &call.gpu_finishes,
outputs,
hdr_env: &call.hdr_env,
};
let bind_group = build_bind_group(&bind_args);
let workgroups = (tuples as u32).div_ceil(WORKGROUP_SIZE as u32);
let _ = compute::dispatch(
&self.ctx.device,
&self.ctx.queue,
bind_args.pipeline,
&bind_group,
(workgroups, 1, 1),
);
let pixel_output = self.pixel_outputs[chunk_index % 2]
.as_ref()
.expect("ensure_pixel_capacity just populated both slots");
let reduce_params = GpuReduceParams {
num_pixels: pixels_this_chunk as u32,
num_samples: spp,
_pad0: 0,
_pad1: 0,
};
let reduce_params_buf = compute::upload(
&self.ctx.device,
"reduce xyz params (wasm32 async)",
std::slice::from_ref(&reduce_params),
BufferUsages::UNIFORM,
);
let reduce_bind_group = compute::bind_buffers(
&self.ctx.device,
"reduce xyz bind group (wasm32 async)",
&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),
);
let (staging, _copy_index) = compute::copy_to_staging::<f32>(
&self.ctx.device,
&self.ctx.queue,
&pixel_output.buffer,
pixels_this_chunk * 3,
"transport out pixel xyz staging (wasm32 async)",
);
staging
}
}
struct AsyncCallInputs<'s, 'a> {
scene: &'a GpuFrameScene<'s>,
pipeline_class: u32,
spp: u32,
sample_offset: u32,
env: EnvironmentParams,
white_balance: Vec3,
gpu_material: GpuGemMaterial,
gpu_finishes: Vec<u32>,
hdr_env: HdrEnvGpuData,
}
impl<'s, 'a> AsyncCallInputs<'s, 'a> {
fn new(
renderer: &GpuFrameRenderer,
scene: &'a GpuFrameScene<'s>,
pipeline_class: u32,
spp: u32,
sample_offset: u32,
) -> Result<Self, GpuFrameError> {
let hdr_env = match scene.environment {
EnvironmentSource::HdrMap(map) => {
if !HdrEnvGpuData::fits_storage_binding(&renderer.ctx.device, map) {
return Err(GpuFrameError::UnsupportedEnvironment);
}
HdrEnvGpuData::upload(&renderer.ctx.device, map)
}
EnvironmentSource::Studio { .. } => HdrEnvGpuData::dummy(&renderer.ctx.device),
};
Ok(Self {
scene,
pipeline_class,
spp,
sample_offset,
env: environment_params(scene.environment),
white_balance: environment_white_balance(scene.environment),
gpu_material: GpuGemMaterial::encode(scene.material),
gpu_finishes: encode_facet_finishes(scene.facet_finishes, scene.planes.len()),
hdr_env,
})
}
const fn chunk_params(
&self,
first_pixel: usize,
pixels_this_chunk: usize,
) -> (GpuCameraParams, GpuTransportParams) {
let scene = self.scene;
let (
env_mode,
temp_k,
spot_mult,
exposure,
light_yaw,
light_pitch,
use_d65,
studio_model,
backdrop,
) = self.env;
let camera_params = GpuCameraParams {
origin: scene.camera.origin.to_array(),
fov_tan: scene.camera.fov_tan,
forward: scene.camera.forward.to_array(),
width: scene.width as f32,
right: scene.camera.right.to_array(),
height: scene.height as f32,
up: scene.camera.up.to_array(),
num_samples: self.spp,
};
let params = GpuTransportParams::new(
pixels_this_chunk as u32,
scene.max_bounces,
self.sample_offset,
env_mode,
0.0,
temp_k,
spot_mult,
exposure,
light_yaw,
light_pitch,
self.white_balance.to_array(),
)
.with_pixel_offset(first_pixel as u32)
.with_debug_buffers_disabled()
.with_studio_use_d65(use_d65)
.with_studio_model(studio_model)
.with_backdrop(backdrop);
(camera_params, params)
}
}
#[allow(
clippy::future_not_send,
reason = "wgpu's web backend's buffer-mapping state is inherently !Send (browser-side \
Rc handles); wasm32-unknown-unknown has no second thread to send this future \
to regardless -- see GpuContext::acquire_async's identical allow"
)]
async fn map_read_async<T: bytemuck::Pod>(
staging: &wgpu::Buffer,
count: usize,
) -> Result<Vec<T>, GpuFrameError> {
let (tx, rx) = futures_channel::oneshot::channel();
staging
.slice(..)
.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
rx.await
.map_err(|_| {
GpuFrameError::DeviceLost(
"wgpu buffer-mapping callback was dropped before it fired".to_string(),
)
})?
.map_err(|e| GpuFrameError::DeviceLost(format!("wgpu buffer mapping failed: {e}")))?;
let out = {
let data = staging.slice(..).get_mapped_range().map_err(|e| {
GpuFrameError::DeviceLost(format!("mapped buffer range could not be read: {e}"))
})?;
bytemuck::cast_slice::<u8, T>(&data).to_vec()
};
debug_assert_eq!(
out.len(),
count,
"staging buffer length must match copy_to_staging's count"
);
staging.unmap();
Ok(out)
}