1use std::num::NonZeroU64;
2
3pub(crate) const UNIFORM_SIZE: u64 = 512;
7const FRAMES: usize = 3;
9const IDLE_PASSES: u8 = 3;
11const SLOTS_PER_BLOCK: u64 = 1024;
13const VERTEX_BLOCK_SIZE: u64 = 256 * 1024;
16
17pub struct HostBuffer {
29 device: wgpu::Device,
30 layout: wgpu::BindGroupLayout,
31 frames: [FrameArena; FRAMES],
32 frame: usize,
33 stride: u64,
34 uniform_block_size: u64,
35 pub(crate) blocks_created: u64,
37}
38
39#[derive(Default)]
40struct FrameArena {
41 uniforms: Vec<Block>,
42 cursor: Cursor,
43 vertices: Vec<Block>,
44 vertex_cursor: Cursor,
45}
46
47#[derive(Default, Clone, Copy)]
48struct Cursor {
49 block: usize,
50 offset: u64,
51}
52
53struct Block {
54 buffer: wgpu::Buffer,
55 bind_group: Option<wgpu::BindGroup>,
57 scratch: Vec<u8>,
58 used: u64,
59 idle: u8,
61}
62
63#[derive(Clone, Copy, Debug)]
65pub(crate) struct DrawSlot {
66 pub block: usize,
67 pub offset: u32,
68}
69
70#[derive(Clone, Copy, Debug)]
72pub(crate) struct VertexSlot {
73 pub block: usize,
74 pub offset: u64,
75 pub bytes: u64,
76}
77
78impl HostBuffer {
79 pub fn new(device: &wgpu::Device) -> Self {
84 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
85 label: Some("valo.host_buffer"),
86 entries: &[wgpu::BindGroupLayoutEntry {
87 binding: 0,
88 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
89 ty: wgpu::BindingType::Buffer {
90 ty: wgpu::BufferBindingType::Uniform,
91 has_dynamic_offset: true,
92 min_binding_size: NonZeroU64::new(UNIFORM_SIZE),
93 },
94 count: None,
95 }],
96 });
97 let stride = (device.limits().min_uniform_buffer_offset_alignment as u64).max(UNIFORM_SIZE);
98 Self {
99 device: device.clone(),
100 layout,
101 frames: Default::default(),
102 frame: 0,
103 stride,
104 uniform_block_size: stride * SLOTS_PER_BLOCK,
105 blocks_created: 0,
106 }
107 }
108
109 pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
114 &self.layout
115 }
116
117 pub fn begin_frame(&mut self) {
123 self.frame = (self.frame + 1) % FRAMES;
124 let arena = &mut self.frames[self.frame];
125 arena.cursor = Cursor::default();
126 arena.vertex_cursor = Cursor::default();
127 for blocks in [&mut arena.uniforms, &mut arena.vertices] {
128 for b in blocks.iter_mut() {
129 b.idle = if b.used > 0 {
130 0
131 } else {
132 b.idle.saturating_add(1)
133 };
134 b.used = 0;
135 }
136 while blocks.len() > 1 && blocks.last().is_some_and(|b| b.idle >= IDLE_PASSES) {
137 blocks.pop();
138 }
139 }
140 }
141
142 pub(crate) fn alloc_uniform(&mut self, bytes: &[u8]) -> DrawSlot {
144 debug_assert!(bytes.len() as u64 <= self.stride);
145 let stride = self.stride;
146 let block_size = self.uniform_block_size;
147 let (device, layout) = (self.device.clone(), self.layout.clone());
148 let arena = &mut self.frames[self.frame];
149
150 advance_cursor(&mut arena.cursor, &arena.uniforms, stride);
151 if arena.cursor.block >= arena.uniforms.len() {
152 arena
153 .uniforms
154 .push(new_uniform_block(&device, &layout, block_size));
155 self.blocks_created += 1;
156 }
157 let cursor = arena.cursor;
158 write_scratch(&mut arena.uniforms[cursor.block], cursor.offset, bytes);
159 arena.cursor.offset += stride;
160 DrawSlot {
161 block: cursor.block,
162 offset: cursor.offset as u32,
163 }
164 }
165
166 pub(crate) fn alloc_vertices(&mut self, bytes: &[u8]) -> VertexSlot {
168 let len = bytes.len() as u64;
169 let block_size = VERTEX_BLOCK_SIZE.max(len);
170 let device = self.device.clone();
171 let arena = &mut self.frames[self.frame];
172
173 advance_cursor(&mut arena.vertex_cursor, &arena.vertices, len);
174 if arena.vertex_cursor.block >= arena.vertices.len() {
175 arena.vertices.push(new_vertex_block(&device, block_size));
176 self.blocks_created += 1;
177 }
178 let cursor = arena.vertex_cursor;
179 write_scratch(&mut arena.vertices[cursor.block], cursor.offset, bytes);
180 arena.vertex_cursor.offset += len.next_multiple_of(4);
181 VertexSlot {
182 block: cursor.block,
183 offset: cursor.offset,
184 bytes: len,
185 }
186 }
187
188 pub fn flush(&mut self, queue: &wgpu::Queue) -> (u64, u64) {
193 let arena = &self.frames[self.frame];
194 let mut written = (0u64, 0u64);
195 for b in &arena.uniforms {
196 written.0 += b.used;
197 }
198 for b in &arena.vertices {
199 written.1 += b.used;
200 }
201 for b in arena.uniforms.iter().chain(arena.vertices.iter()) {
202 if b.used > 0 {
203 queue.write_buffer(&b.buffer, 0, &b.scratch[..b.used as usize]);
204 }
205 }
206 written
207 }
208
209 pub(crate) fn report(&self) -> crate::PoolReport {
211 let mut count = 0u32;
212 let mut bytes = 0u64;
213 for arena in &self.frames {
214 for b in arena.uniforms.iter().chain(arena.vertices.iter()) {
215 count += 1;
216 bytes += b.scratch.len() as u64;
217 }
218 }
219 crate::PoolReport { count, bytes }
220 }
221
222 pub(crate) fn bind_group(&self, block: usize) -> &wgpu::BindGroup {
223 self.frames[self.frame].uniforms[block]
224 .bind_group
225 .as_ref()
226 .expect("uniform blocks always carry a bind group")
227 }
228
229 pub(crate) fn vertex_buffer(&self, block: usize) -> &wgpu::Buffer {
230 &self.frames[self.frame].vertices[block].buffer
231 }
232}
233
234fn advance_cursor(cursor: &mut Cursor, blocks: &[Block], needed: u64) {
239 while let Some(block) = blocks.get(cursor.block) {
240 if cursor.offset + needed <= block.scratch.len() as u64 {
241 return;
242 }
243 cursor.block += 1;
244 cursor.offset = 0;
245 }
246}
247
248fn new_uniform_block(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, size: u64) -> Block {
249 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
250 label: Some("valo.host_buffer.uniforms"),
251 size,
252 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
253 mapped_at_creation: false,
254 });
255 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
256 label: Some("valo.host_buffer.uniforms"),
257 layout,
258 entries: &[wgpu::BindGroupEntry {
259 binding: 0,
260 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
261 buffer: &buffer,
262 offset: 0,
263 size: NonZeroU64::new(UNIFORM_SIZE),
264 }),
265 }],
266 });
267 Block {
268 buffer,
269 bind_group: Some(bind_group),
270 scratch: vec![0; size as usize],
271 used: 0,
272 idle: 0,
273 }
274}
275
276fn new_vertex_block(device: &wgpu::Device, size: u64) -> Block {
277 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
278 label: Some("valo.host_buffer.vertices"),
279 size,
280 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
281 mapped_at_creation: false,
282 });
283 Block {
284 buffer,
285 bind_group: None,
286 scratch: vec![0; size as usize],
287 used: 0,
288 idle: 0,
289 }
290}
291
292fn write_scratch(block: &mut Block, offset: u64, bytes: &[u8]) {
293 block.scratch[offset as usize..offset as usize + bytes.len()].copy_from_slice(bytes);
294 block.used = block.used.max(offset + bytes.len() as u64);
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn headless() -> Option<wgpu::Device> {
302 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
303 let adapter =
304 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
305 .ok()?;
306 let (device, _queue) =
307 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()?;
308 Some(device)
309 }
310
311 #[test]
316 fn retained_mixed_size_blocks_never_overrun() {
317 let Some(device) = headless() else {
318 eprintln!("SKIP retained_mixed_size_blocks_never_overrun: no GPU adapter");
319 return;
320 };
321 let mut host = HostBuffer::new(&device);
322 host.begin_frame();
323 host.alloc_vertices(&vec![1u8; 1024 * 1024]);
325 host.alloc_vertices(&[2u8; 64]);
326 for _ in 0..FRAMES {
328 host.begin_frame();
329 }
330 let a = host.alloc_vertices(&vec![3u8; 200 * 1024]);
331 let b = host.alloc_vertices(&vec![4u8; 800 * 1024]); assert_eq!(a.block, 0);
333 assert_eq!(b.block, 0, "800 KB still fits the 1 MB block");
334
335 let c = host.alloc_vertices(&vec![5u8; 900 * 1024]);
338 assert_eq!(c.bytes, 900 * 1024);
339 assert!(c.block >= 2, "small retained block is skipped, not overrun");
340 }
341}