use std::time::Duration;
use wgpu::util::DeviceExt;
pub const GPU_WAIT_TIMEOUT: Duration = Duration::from_secs(4);
#[derive(Debug)]
pub enum ComputeWaitError {
Poll(wgpu::PollError),
MapTimeout,
Map(wgpu::BufferAsyncError),
MapRange(wgpu::MapRangeError),
}
impl std::fmt::Display for ComputeWaitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Poll(e) => write!(f, "device poll failed or timed out: {e}"),
Self::MapTimeout => write!(
f,
"buffer-mapping callback did not fire within {GPU_WAIT_TIMEOUT:?}"
),
Self::Map(e) => write!(f, "buffer mapping failed: {e}"),
Self::MapRange(e) => write!(f, "mapped buffer range could not be read: {e}"),
}
}
}
impl std::error::Error for ComputeWaitError {}
#[must_use]
pub fn create_compute_pipeline(
device: &wgpu::Device,
label: &str,
wgsl_source: &str,
entry_point: &str,
) -> wgpu::ComputePipeline {
create_compute_pipeline_with_constants(device, label, wgsl_source, entry_point, &[])
}
#[must_use]
pub fn create_compute_pipeline_with_constants(
device: &wgpu::Device,
label: &str,
wgsl_source: &str,
entry_point: &str,
constants: &[(&str, f64)],
) -> wgpu::ComputePipeline {
create_compute_pipeline_cached(device, None, label, wgsl_source, entry_point, constants)
}
struct CompileTrace<'a> {
label: &'a str,
entry_point: &'a str,
constants: &'a [(&'a str, f64)],
}
impl Drop for CompileTrace<'_> {
fn drop(&mut self) {
if std::thread::panicking() {
eprintln!(
"panic while compiling compute pipeline `{}` (entry point `{}`, pipeline constants {:?})",
self.label, self.entry_point, self.constants
);
}
}
}
#[must_use]
pub fn create_compute_pipeline_cached(
device: &wgpu::Device,
cache: Option<&wgpu::PipelineCache>,
label: &str,
wgsl_source: &str,
entry_point: &str,
constants: &[(&str, f64)],
) -> wgpu::ComputePipeline {
let _trace = CompileTrace {
label,
entry_point,
constants,
};
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(label),
source: wgpu::ShaderSource::Wgsl(wgsl_source.into()),
});
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(label),
layout: None,
module: &shader,
entry_point: Some(entry_point),
compilation_options: wgpu::PipelineCompilationOptions {
constants,
zero_initialize_workgroup_memory: true,
},
cache,
})
}
#[must_use]
pub fn create_pipeline_cache(
device: &wgpu::Device,
adapter_info: &wgpu::AdapterInfo,
) -> Option<wgpu::PipelineCache> {
if !device.features().contains(wgpu::Features::PIPELINE_CACHE) {
return None;
}
let blob = super::pipeline_cache::load_blob(adapter_info);
let cache = unsafe {
device.create_pipeline_cache(&wgpu::PipelineCacheDescriptor {
label: Some("indicatrix gpu pipeline cache"),
data: blob.as_deref(),
fallback: true,
})
};
Some(cache)
}
#[must_use]
pub fn upload<T: bytemuck::Pod>(
device: &wgpu::Device,
label: &str,
data: &[T],
usage: wgpu::BufferUsages,
) -> wgpu::Buffer {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(data),
usage,
})
}
#[must_use]
pub fn zeroed_buffer<T: bytemuck::Pod>(
device: &wgpu::Device,
label: &str,
count: usize,
usage: wgpu::BufferUsages,
) -> wgpu::Buffer {
let zeros = vec![T::zeroed(); count];
upload(device, label, &zeros, usage)
}
#[must_use]
pub fn readback<T: bytemuck::Pod>(
device: &wgpu::Device,
queue: &wgpu::Queue,
source: &wgpu::Buffer,
count: usize,
) -> Vec<T> {
let byte_len = (count * size_of::<T>()) as wgpu::BufferAddress;
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("indicatrix gpu self-test readback staging buffer"),
size: byte_len,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("indicatrix gpu self-test readback encoder"),
});
encoder.copy_buffer_to_buffer(source, 0, &staging, 0, byte_len);
queue.submit(Some(encoder.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
device
.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(GPU_WAIT_TIMEOUT),
})
.expect("device poll failed or timed out during readback");
rx.recv_timeout(GPU_WAIT_TIMEOUT)
.expect("map_async callback did not fire within GPU_WAIT_TIMEOUT")
.expect("failed to map readback staging buffer");
let out = {
let data = slice
.get_mapped_range()
.expect("staging buffer should be mapped at this point");
bytemuck::cast_slice::<u8, T>(&data).to_vec()
};
staging.unmap();
out
}
pub fn dispatch_and_wait(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &wgpu::ComputePipeline,
bind_group: &wgpu::BindGroup,
workgroups: (u32, u32, u32),
) {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("indicatrix gpu self-test dispatch encoder"),
});
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("indicatrix gpu self-test compute pass"),
timestamp_writes: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, bind_group, &[]);
pass.dispatch_workgroups(workgroups.0, workgroups.1, workgroups.2);
}
queue.submit(Some(encoder.finish()));
device
.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(GPU_WAIT_TIMEOUT),
})
.expect("device poll failed or timed out during dispatch");
}
#[must_use]
pub fn dispatch(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &wgpu::ComputePipeline,
bind_group: &wgpu::BindGroup,
workgroups: (u32, u32, u32),
) -> wgpu::SubmissionIndex {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("indicatrix gpu pipelined dispatch encoder"),
});
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("indicatrix gpu pipelined compute pass"),
timestamp_writes: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, bind_group, &[]);
pass.dispatch_workgroups(workgroups.0, workgroups.1, workgroups.2);
}
queue.submit(Some(encoder.finish()))
}
#[must_use]
pub fn copy_to_staging<T: bytemuck::Pod>(
device: &wgpu::Device,
queue: &wgpu::Queue,
source: &wgpu::Buffer,
count: usize,
label: &str,
) -> (wgpu::Buffer, wgpu::SubmissionIndex) {
let byte_len = (count * size_of::<T>()) as wgpu::BufferAddress;
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: byte_len,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("indicatrix gpu pipelined readback copy encoder"),
});
encoder.copy_buffer_to_buffer(source, 0, &staging, 0, byte_len);
let index = queue.submit(Some(encoder.finish()));
(staging, index)
}
#[must_use]
pub fn begin_map_read(
staging: &wgpu::Buffer,
) -> std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>> {
let (tx, rx) = std::sync::mpsc::channel();
staging
.slice(..)
.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
rx
}
pub fn finish_map_read<T: bytemuck::Pod>(
device: &wgpu::Device,
staging: &wgpu::Buffer,
copy_index: wgpu::SubmissionIndex,
rx: &std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
) -> Result<Vec<T>, ComputeWaitError> {
device
.poll(wgpu::PollType::Wait {
submission_index: Some(copy_index),
timeout: Some(GPU_WAIT_TIMEOUT),
})
.map_err(ComputeWaitError::Poll)?;
rx.recv_timeout(GPU_WAIT_TIMEOUT)
.map_err(|_| ComputeWaitError::MapTimeout)?
.map_err(ComputeWaitError::Map)?;
let out = {
let data = staging
.slice(..)
.get_mapped_range()
.map_err(ComputeWaitError::MapRange)?;
bytemuck::cast_slice::<u8, T>(&data).to_vec()
};
staging.unmap();
Ok(out)
}
#[must_use]
pub fn bind_buffers(
device: &wgpu::Device,
label: &str,
pipeline: &wgpu::ComputePipeline,
buffers: &[(u32, &wgpu::Buffer)],
) -> wgpu::BindGroup {
let layout = pipeline.get_bind_group_layout(0);
let entries: Vec<wgpu::BindGroupEntry<'_>> = buffers
.iter()
.map(|(binding, buffer)| wgpu::BindGroupEntry {
binding: *binding,
resource: buffer.as_entire_binding(),
})
.collect();
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: &layout,
entries: &entries,
})
}
#[must_use]
pub fn bind_buffers_sized(
device: &wgpu::Device,
label: &str,
pipeline: &wgpu::ComputePipeline,
buffers: &[(u32, &wgpu::Buffer, Option<wgpu::BufferAddress>)],
) -> wgpu::BindGroup {
let layout = pipeline.get_bind_group_layout(0);
let entries: Vec<wgpu::BindGroupEntry<'_>> = buffers
.iter()
.map(|(binding, buffer, size)| wgpu::BindGroupEntry {
binding: *binding,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer,
offset: 0,
size: size.and_then(wgpu::BufferSize::new),
}),
})
.collect();
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout: &layout,
entries: &entries,
})
}
pub fn finish_map_read_into<T: bytemuck::Pod>(
device: &wgpu::Device,
staging: &wgpu::Buffer,
copy_index: wgpu::SubmissionIndex,
rx: &std::sync::mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
out: &mut Vec<T>,
) -> Result<(), ComputeWaitError> {
device
.poll(wgpu::PollType::Wait {
submission_index: Some(copy_index),
timeout: Some(GPU_WAIT_TIMEOUT),
})
.map_err(ComputeWaitError::Poll)?;
rx.recv_timeout(GPU_WAIT_TIMEOUT)
.map_err(|_| ComputeWaitError::MapTimeout)?
.map_err(ComputeWaitError::Map)?;
{
let data = staging
.slice(..)
.get_mapped_range()
.map_err(ComputeWaitError::MapRange)?;
let src: &[T] = bytemuck::cast_slice(&data);
out.clear();
out.extend_from_slice(src);
}
staging.unmap();
Ok(())
}