use std::num::NonZeroU64;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub(crate) const UNIFORM_SIZE: u64 = 512;
const FRAMES: usize = 3;
const IDLE_PASSES: u8 = 3;
const SLOTS_PER_BLOCK: u64 = 1024;
const VERTEX_BLOCK_SIZE: u64 = 256 * 1024;
pub struct HostBuffer {
device: wgpu::Device,
layout: wgpu::BindGroupLayout,
frames: [FrameArena; FRAMES],
frame: usize,
stride: u64,
uniform_block_size: u64,
mapped: bool,
pub(crate) blocks_created: u64,
}
#[derive(Default)]
struct FrameArena {
uniforms: Vec<Block>,
cursor: Cursor,
vertices: Vec<Block>,
vertex_cursor: Cursor,
}
#[derive(Default, Clone, Copy)]
struct Cursor {
block: usize,
offset: u64,
}
struct Block {
buffer: wgpu::Buffer,
bind_group: Option<wgpu::BindGroup>,
scratch: Vec<u8>,
used: u64,
idle: u8,
writable: Arc<AtomicBool>,
remap: bool,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct DrawSlot {
pub block: usize,
pub offset: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct VertexSlot {
pub block: usize,
pub offset: u64,
pub bytes: u64,
}
impl HostBuffer {
pub fn new(device: &wgpu::Device) -> Self {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("valo.host_buffer"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: NonZeroU64::new(UNIFORM_SIZE),
},
count: None,
}],
});
let stride = (device.limits().min_uniform_buffer_offset_alignment as u64).max(UNIFORM_SIZE);
Self {
device: device.clone(),
layout,
frames: Default::default(),
frame: 0,
stride,
uniform_block_size: stride * SLOTS_PER_BLOCK,
mapped: device
.features()
.contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS),
blocks_created: 0,
}
}
pub fn after_submit(&mut self) {
if !self.mapped {
return;
}
let arena = &mut self.frames[self.frame];
for b in arena.uniforms.iter_mut().chain(arena.vertices.iter_mut()) {
if !b.remap {
continue;
}
b.remap = false;
let writable = Arc::clone(&b.writable);
b.buffer
.slice(..)
.map_async(wgpu::MapMode::Write, move |result| {
if result.is_ok() {
writable.store(true, Ordering::Release);
}
});
}
}
pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
&self.layout
}
pub fn begin_frame(&mut self) {
if self.mapped {
let _ = self.device.poll(wgpu::PollType::Poll);
}
self.frame = (self.frame + 1) % FRAMES;
let arena = &mut self.frames[self.frame];
arena.cursor = Cursor::default();
arena.vertex_cursor = Cursor::default();
for blocks in [&mut arena.uniforms, &mut arena.vertices] {
for b in blocks.iter_mut() {
b.idle = if b.used > 0 {
0
} else {
b.idle.saturating_add(1)
};
b.used = 0;
}
while blocks.len() > 1 && blocks.last().is_some_and(|b| b.idle >= IDLE_PASSES) {
blocks.pop();
}
}
}
pub(crate) fn alloc_uniform(&mut self, bytes: &[u8]) -> DrawSlot {
debug_assert!(bytes.len() as u64 <= self.stride);
let stride = self.stride;
let block_size = self.uniform_block_size;
let (device, layout, mapped) = (self.device.clone(), self.layout.clone(), self.mapped);
let arena = &mut self.frames[self.frame];
advance_cursor(&mut arena.cursor, &arena.uniforms, stride, mapped);
if arena.cursor.block >= arena.uniforms.len() {
arena
.uniforms
.push(new_uniform_block(&device, &layout, block_size, mapped));
self.blocks_created += 1;
}
let cursor = arena.cursor;
write_scratch(&mut arena.uniforms[cursor.block], cursor.offset, bytes);
arena.cursor.offset += stride;
DrawSlot {
block: cursor.block,
offset: cursor.offset as u32,
}
}
pub(crate) fn alloc_vertices(&mut self, bytes: &[u8]) -> VertexSlot {
let len = bytes.len() as u64;
let block_size = VERTEX_BLOCK_SIZE.max(len);
let (device, mapped) = (self.device.clone(), self.mapped);
let arena = &mut self.frames[self.frame];
advance_cursor(&mut arena.vertex_cursor, &arena.vertices, len, mapped);
if arena.vertex_cursor.block >= arena.vertices.len() {
arena
.vertices
.push(new_vertex_block(&device, block_size, mapped));
self.blocks_created += 1;
}
let cursor = arena.vertex_cursor;
write_scratch(&mut arena.vertices[cursor.block], cursor.offset, bytes);
arena.vertex_cursor.offset += len.next_multiple_of(4);
VertexSlot {
block: cursor.block,
offset: cursor.offset,
bytes: len,
}
}
pub fn flush(&mut self, queue: &wgpu::Queue) -> (u64, u64) {
let mapped = self.mapped;
let arena = &mut self.frames[self.frame];
let mut written = (0u64, 0u64);
for b in &arena.uniforms {
written.0 += b.used;
}
for b in &arena.vertices {
written.1 += b.used;
}
for b in arena.uniforms.iter_mut().chain(arena.vertices.iter_mut()) {
if b.used == 0 {
continue;
}
if mapped {
let mut view = b
.buffer
.slice(..b.used)
.get_mapped_range_mut()
.expect("a block allocated from is mapped for writing");
view.copy_from_slice(&b.scratch[..b.used as usize]);
drop(view);
b.buffer.unmap();
b.writable.store(false, Ordering::Release);
b.remap = true;
} else {
queue.write_buffer(&b.buffer, 0, &b.scratch[..b.used as usize]);
}
}
written
}
pub(crate) fn report(&self) -> crate::PoolReport {
let mut count = 0u32;
let mut bytes = 0u64;
for arena in &self.frames {
for b in arena.uniforms.iter().chain(arena.vertices.iter()) {
count += 1;
bytes += b.scratch.len() as u64;
}
}
crate::PoolReport { count, bytes }
}
pub(crate) fn bind_group(&self, block: usize) -> &wgpu::BindGroup {
self.frames[self.frame].uniforms[block]
.bind_group
.as_ref()
.expect("uniform blocks always carry a bind group")
}
pub(crate) fn vertex_buffer(&self, block: usize) -> &wgpu::Buffer {
&self.frames[self.frame].vertices[block].buffer
}
}
fn advance_cursor(cursor: &mut Cursor, blocks: &[Block], needed: u64, mapped: bool) {
while let Some(block) = blocks.get(cursor.block) {
let writable = !mapped || block.writable.load(Ordering::Acquire);
if writable && cursor.offset + needed <= block.scratch.len() as u64 {
return;
}
cursor.block += 1;
cursor.offset = 0;
}
}
fn block_usage(base: wgpu::BufferUsages, mapped: bool) -> wgpu::BufferUsages {
if mapped {
base | wgpu::BufferUsages::MAP_WRITE
} else {
base | wgpu::BufferUsages::COPY_DST
}
}
fn new_uniform_block(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
size: u64,
mapped: bool,
) -> Block {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("valo.host_buffer.uniforms"),
size,
usage: block_usage(wgpu::BufferUsages::UNIFORM, mapped),
mapped_at_creation: mapped,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("valo.host_buffer.uniforms"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &buffer,
offset: 0,
size: NonZeroU64::new(UNIFORM_SIZE),
}),
}],
});
Block {
buffer,
bind_group: Some(bind_group),
scratch: vec![0; size as usize],
used: 0,
idle: 0,
writable: Arc::new(AtomicBool::new(mapped)),
remap: false,
}
}
fn new_vertex_block(device: &wgpu::Device, size: u64, mapped: bool) -> Block {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("valo.host_buffer.vertices"),
size,
usage: block_usage(wgpu::BufferUsages::VERTEX, mapped),
mapped_at_creation: mapped,
});
Block {
buffer,
bind_group: None,
scratch: vec![0; size as usize],
used: 0,
idle: 0,
writable: Arc::new(AtomicBool::new(mapped)),
remap: false,
}
}
fn write_scratch(block: &mut Block, offset: u64, bytes: &[u8]) {
block.scratch[offset as usize..offset as usize + bytes.len()].copy_from_slice(bytes);
block.used = block.used.max(offset + bytes.len() as u64);
}
#[cfg(test)]
mod tests {
use super::*;
fn mappable_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::default();
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
if !adapter
.features()
.contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS)
{
return None;
}
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::MAPPABLE_PRIMARY_BUFFERS,
..Default::default()
}))
.ok()
}
#[test]
fn a_mapped_block_is_written_in_place_and_writable_again_by_its_next_turn() {
let Some((device, queue)) = mappable_device() else {
eprintln!("SKIP a_mapped_block_is_written_in_place: no mappable adapter");
return;
};
let mut host = HostBuffer::new(&device);
assert!(host.mapped);
host.begin_frame();
let slot = host.alloc_uniform(&[7u8; 64]);
assert_eq!(
host.flush(&queue),
(64, 0),
"the bytes used, not the stride"
);
let block = &host.frames[host.frame].uniforms[slot.block];
assert!(
!block.writable.load(Ordering::Acquire),
"unmapped for the GPU"
);
host.after_submit();
for _ in 0..FRAMES {
host.begin_frame();
}
let block = &host.frames[host.frame].uniforms[slot.block];
assert!(block.writable.load(Ordering::Acquire));
assert_eq!(host.blocks_created, 1);
}
fn headless() -> Option<wgpu::Device> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter =
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.ok()?;
let (device, _queue) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()?;
Some(device)
}
#[test]
fn retained_mixed_size_blocks_never_overrun() {
let Some(device) = headless() else {
eprintln!("SKIP retained_mixed_size_blocks_never_overrun: no GPU adapter");
return;
};
let mut host = HostBuffer::new(&device);
host.begin_frame();
host.alloc_vertices(&vec![1u8; 1024 * 1024]);
host.alloc_vertices(&[2u8; 64]);
for _ in 0..FRAMES {
host.begin_frame();
}
let a = host.alloc_vertices(&vec![3u8; 200 * 1024]);
let b = host.alloc_vertices(&vec![4u8; 800 * 1024]); assert_eq!(a.block, 0);
assert_eq!(b.block, 0, "800 KB still fits the 1 MB block");
let c = host.alloc_vertices(&vec![5u8; 900 * 1024]);
assert_eq!(c.bytes, 900 * 1024);
assert!(c.block >= 2, "small retained block is skipped, not overrun");
}
}