use crate::error::{GpuError, Result};
#[must_use]
pub fn create_storage_buffer(
device: &wgpu::Device,
data: &[u8],
label: &str,
read_only: bool,
) -> wgpu::Buffer {
tracing::debug!(
label,
size = data.len(),
read_only,
"creating storage buffer"
);
use wgpu::util::DeviceExt;
let mut usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST;
if !read_only {
usage |= wgpu::BufferUsages::COPY_SRC;
}
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: data,
usage,
})
}
#[must_use]
pub fn create_storage_buffer_empty(
device: &wgpu::Device,
size: u64,
label: &str,
read_only: bool,
) -> wgpu::Buffer {
tracing::debug!(label, size, read_only, "creating empty storage buffer");
let mut usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST;
if !read_only {
usage |= wgpu::BufferUsages::COPY_SRC;
}
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage,
mapped_at_creation: false,
})
}
#[must_use]
pub fn create_uniform_buffer(device: &wgpu::Device, data: &[u8], label: &str) -> wgpu::Buffer {
tracing::debug!(label, size = data.len(), "creating uniform buffer");
use wgpu::util::DeviceExt;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: data,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
})
}
#[must_use]
pub fn create_staging_buffer(device: &wgpu::Device, size: u64, label: &str) -> wgpu::Buffer {
tracing::debug!(label, size, "creating staging buffer");
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
pub fn read_buffer(
device: &wgpu::Device,
queue: &wgpu::Queue,
source: &wgpu::Buffer,
size: u64,
) -> Result<Vec<u8>> {
read_buffer_async(device, queue, source, size).finish(device)
}
pub fn read_buffer_async(
device: &wgpu::Device,
queue: &wgpu::Queue,
source: &wgpu::Buffer,
size: u64,
) -> PendingReadback {
tracing::debug!(size, "GPU buffer readback submitted");
let staging = create_staging_buffer(device, size, "readback_staging");
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("readback_encoder"),
});
encoder.copy_buffer_to_buffer(source, 0, &staging, 0, size);
queue.submit(std::iter::once(encoder.finish()));
PendingReadback { staging }
}
#[must_use = "readback submitted but never completed — call .finish()"]
pub struct PendingReadback {
staging: wgpu::Buffer,
}
impl PendingReadback {
pub fn finish(self, device: &wgpu::Device) -> Result<Vec<u8>> {
let slice = self.staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
let _ = device.poll(wgpu::PollType::Wait {
timeout: None,
submission_index: None,
});
rx.recv()
.map_err(|e| {
tracing::error!("buffer readback channel error: {e}");
let _ = e;
GpuError::ReadbackChannel
})?
.map_err(|e| {
tracing::error!("buffer readback map failed: {e}");
GpuError::ReadbackMap(e)
})?;
let data = slice.get_mapped_range();
let result = data.to_vec();
drop(data);
self.staging.unmap();
Ok(result)
}
}
pub fn read_buffer_typed<T: bytemuck::Pod>(
device: &wgpu::Device,
queue: &wgpu::Queue,
source: &wgpu::Buffer,
count: usize,
) -> Result<Vec<T>> {
let size = count
.checked_mul(std::mem::size_of::<T>())
.ok_or_else(|| GpuError::Buffer("read_buffer_typed: size overflow".into()))?
as u64;
let bytes = read_buffer(device, queue, source, size)?;
Ok(bytemuck::cast_slice(&bytes).to_vec())
}
#[cfg(feature = "graphics")]
#[must_use]
pub fn create_vertex_buffer<T: bytemuck::Pod>(
device: &wgpu::Device,
vertices: &[T],
label: &str,
) -> wgpu::Buffer {
tracing::debug!(label, count = vertices.len(), "creating vertex buffer");
use wgpu::util::DeviceExt;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
})
}
#[cfg(feature = "graphics")]
#[must_use]
pub fn create_index_buffer<T: bytemuck::Pod>(
device: &wgpu::Device,
indices: &[T],
label: &str,
) -> wgpu::Buffer {
tracing::debug!(label, count = indices.len(), "creating index buffer");
use wgpu::util::DeviceExt;
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(indices),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
})
}
#[must_use]
pub fn create_dispatch_indirect_buffer(
device: &wgpu::Device,
workgroups: [u32; 3],
label: &str,
) -> wgpu::Buffer {
use wgpu::util::DeviceExt;
tracing::debug!(label, ?workgroups, "creating dispatch indirect buffer");
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(&workgroups),
usage: wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
})
}
#[cfg(feature = "graphics")]
#[must_use]
pub fn create_draw_indirect_buffer(
device: &wgpu::Device,
vertex_count: u32,
instance_count: u32,
label: &str,
) -> wgpu::Buffer {
use wgpu::util::DeviceExt;
tracing::debug!(
label,
vertex_count,
instance_count,
"creating draw indirect buffer"
);
let data = [vertex_count, instance_count, 0u32, 0u32];
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(&data),
usage: wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
})
}
#[cfg(feature = "graphics")]
#[must_use]
pub fn create_draw_indexed_indirect_buffer(
device: &wgpu::Device,
index_count: u32,
instance_count: u32,
label: &str,
) -> wgpu::Buffer {
use wgpu::util::DeviceExt;
tracing::debug!(
label,
index_count,
instance_count,
"creating indexed draw indirect buffer"
);
let data = [index_count, instance_count, 0u32, 0u32, 0u32];
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(&data),
usage: wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
})
}
pub struct GrowableBuffer {
pub buffer: wgpu::Buffer,
pub count: u32,
capacity: usize,
usage: wgpu::BufferUsages,
label: String,
element_size: usize,
generation: u64,
}
impl GrowableBuffer {
pub fn new(
device: &wgpu::Device,
capacity: usize,
element_size: usize,
usage: wgpu::BufferUsages,
label: impl Into<String>,
) -> Self {
let label = label.into();
let capacity = capacity.max(16);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(&label),
size: (capacity.saturating_mul(element_size)) as u64,
usage: usage | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
buffer,
count: 0,
capacity,
usage: usage | wgpu::BufferUsages::COPY_DST,
label,
element_size,
generation: 0,
}
}
pub fn update<T: bytemuck::Pod>(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
data: &[T],
) {
self.count = data.len().min(u32::MAX as usize) as u32;
if data.is_empty() {
return;
}
if data.len() > self.capacity {
let new_capacity = data.len().saturating_mul(3).saturating_div(2).max(16);
tracing::debug!(
old_capacity = self.capacity,
new_capacity,
label = %self.label,
"growable buffer regrow"
);
self.capacity = new_capacity;
self.generation += 1;
self.buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(&self.label),
size: (self.capacity.saturating_mul(self.element_size)) as u64,
usage: self.usage,
mapped_at_creation: false,
});
}
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
}
#[must_use]
#[inline]
pub fn count(&self) -> u32 {
self.count
}
#[must_use]
#[inline]
pub fn capacity(&self) -> usize {
self.capacity
}
#[must_use]
#[inline]
pub fn generation(&self) -> u64 {
self.generation
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn storage_buffer_usage_read_only() {
let _size = std::mem::size_of::<wgpu::Buffer>();
}
#[test]
fn staging_buffer_label() {
let _size = std::mem::size_of::<wgpu::BufferDescriptor<'_>>();
}
#[test]
fn growable_buffer_types() {
let _size = std::mem::size_of::<GrowableBuffer>();
assert!(std::mem::size_of::<GrowableBuffer>() > 0);
}
#[test]
fn growable_buffer_growth_formula() {
let grow = |len: usize| (len * 3 / 2).max(16);
assert_eq!(grow(1), 16);
assert_eq!(grow(10), 16);
assert_eq!(grow(16), 24);
assert_eq!(grow(20), 30);
assert_eq!(grow(100), 150);
assert_eq!(grow(1000), 1500);
}
#[test]
fn growable_buffer_min_capacity() {
fn apply_min(cap: usize) -> usize {
cap.max(16)
}
assert_eq!(apply_min(0), 16);
assert_eq!(apply_min(5), 16);
assert_eq!(apply_min(16), 16);
assert_eq!(apply_min(100), 100);
}
#[test]
fn growable_buffer_empty_update_count() {
let count: u32 = 0usize as u32;
assert_eq!(count, 0);
}
#[test]
fn growable_buffer_count_tracking() {
let data_len = 42usize;
let count = data_len as u32;
assert_eq!(count, 42);
}
fn try_gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
let ctx = pollster::block_on(crate::context::GpuContext::new()).ok()?;
Some((ctx.device, ctx.queue))
}
#[test]
fn gpu_create_storage_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let data: [f32; 4] = [1.0, 2.0, 3.0, 4.0];
let buf =
create_storage_buffer(&device, bytemuck::cast_slice(&data), "test_storage", false);
assert_eq!(buf.size(), 16);
}
#[test]
fn gpu_create_storage_buffer_empty() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let buf = create_storage_buffer_empty(&device, 256, "test_empty", true);
assert_eq!(buf.size(), 256);
}
#[test]
fn gpu_create_uniform_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let data: [f32; 4] = [0.0; 4];
let buf = create_uniform_buffer(&device, bytemuck::cast_slice(&data), "test_uniform");
assert_eq!(buf.size(), 16);
}
#[test]
fn gpu_create_staging_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let buf = create_staging_buffer(&device, 1024, "test_staging");
assert_eq!(buf.size(), 1024);
}
#[test]
fn gpu_read_buffer_roundtrip() {
let Some((device, queue)) = try_gpu() else {
return;
};
let data: [f32; 4] = [1.0, 2.0, 3.0, 4.0];
let buf =
create_storage_buffer(&device, bytemuck::cast_slice(&data), "readback_test", false);
let result = read_buffer(&device, &queue, &buf, 16).unwrap();
let output: &[f32] = bytemuck::cast_slice(&result);
assert_eq!(output, &[1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn gpu_read_buffer_async_roundtrip() {
let Some((device, queue)) = try_gpu() else {
return;
};
let data: [f32; 2] = [42.0, -1.0];
let buf = create_storage_buffer(&device, bytemuck::cast_slice(&data), "async_test", false);
let pending = read_buffer_async(&device, &queue, &buf, 8);
let result = pending.finish(&device).unwrap();
let output: &[f32] = bytemuck::cast_slice(&result);
assert_eq!(output, &[42.0, -1.0]);
}
#[test]
fn gpu_read_buffer_typed() {
let Some((device, queue)) = try_gpu() else {
return;
};
let data: [u32; 8] = [10, 20, 30, 40, 50, 60, 70, 80];
let buf = create_storage_buffer(&device, bytemuck::cast_slice(&data), "typed_test", false);
let result: Vec<u32> = read_buffer_typed(&device, &queue, &buf, 8).unwrap();
assert_eq!(result, vec![10, 20, 30, 40, 50, 60, 70, 80]);
}
#[test]
fn gpu_growable_buffer_update_and_grow() {
let Some((device, queue)) = try_gpu() else {
return;
};
let mut buf = GrowableBuffer::new(
&device,
4, std::mem::size_of::<f32>(),
wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
"grow_test",
);
assert_eq!(buf.count(), 0);
let gen0 = buf.generation();
let small: [f32; 4] = [1.0, 2.0, 3.0, 4.0];
buf.update(&device, &queue, &small);
assert_eq!(buf.count(), 4);
let large: [f32; 32] = [0.5; 32];
buf.update(&device, &queue, &large);
assert_eq!(buf.count(), 32);
assert!(buf.generation() > gen0);
}
#[test]
#[cfg(feature = "graphics")]
fn gpu_create_vertex_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let data: [f32; 8] = [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
let buf = create_vertex_buffer(&device, &data, "test_vertex");
assert_eq!(buf.size(), (8 * std::mem::size_of::<f32>()) as u64);
}
#[test]
#[cfg(feature = "graphics")]
fn gpu_create_index_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let data: [u16; 6] = [0, 1, 2, 2, 3, 0];
let buf = create_index_buffer(&device, &data, "test_index");
assert!(buf.size() >= (6 * std::mem::size_of::<u16>()) as u64);
}
#[test]
fn gpu_create_dispatch_indirect_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let buf = create_dispatch_indirect_buffer(&device, [64, 1, 1], "test_dispatch");
assert_eq!(buf.size(), 12);
}
#[test]
#[cfg(feature = "graphics")]
fn gpu_create_draw_indirect_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let buf = create_draw_indirect_buffer(&device, 100, 1, "test_draw");
assert_eq!(buf.size(), 16);
}
#[test]
#[cfg(feature = "graphics")]
fn gpu_create_draw_indexed_indirect_buffer() {
let Some((device, _queue)) = try_gpu() else {
return;
};
let buf = create_draw_indexed_indirect_buffer(&device, 36, 1, "test_draw_indexed");
assert_eq!(buf.size(), 20);
}
}