Hello community,
I'm currently learning Rust, went through The Book and made some basic data structures to learn the ins and outs and I'm confronting myself to some real world problems by trying to make a small 3D application. Staying in theory land doesn't help me a lot anymore, I need to face real problems and find solutions to them to get better.
I'm doing this using the amazing WGPU (which I've used from C in the past so I'm in familliar territory). Anyway I'm progressing slowly but surely and now I'm trying to pool my Buffers (because allocating a bunch of small Buffers is slow and you should avoid it) but I'm angering the borrow checker.
I've basically done the most basic thing I thought of: a linear allocator
// Creates a buffer when needed, keeps allocating into the created buffer,
// and creates a new one if size too small.
pub struct BufferPool {
buffers: Vec<wgpu::Buffer>,
current_position_in_buffer: u64,
kind: wgpu::BufferUsages,
}
impl BufferPool {
fn grow(&mut self, size: u64, device: &wgpu::Device) {
self.buffers.push(device.create_buffer(&wgpu::BufferDescriptor { label: Some("Buffer Pool"), size: max(MIN_BUFFER_SIZE, size), usage: self.kind, mapped_at_creation: false }));
self.current_position_in_buffer = 0;
}
fn maybe_grow(&mut self, size: u64, device: &wgpu::Device) {
if let Some(buf) = self.buffers.last() {
if size > (buf.size() - self.current_position_in_buffer) {
self.grow(size, device);
}
} else { // No buffers yet
self.grow(size, device);
}
}
// Here's the only external call:
pub fn load_data<T: Sized> (&mut self, data: &Vec<T>, device: &wgpu::Device, queue: &wgpu::Queue) -> wgpu::BufferSlice {
let size = (data.len() * size_of::<T>()) as u64;
self.maybe_grow(size, device);
let offset = self.current_position_in_buffer;
self.current_position_in_buffer += size;
let buf = self.buffers.last().unwrap(); // Wish I didn't have to do this...
let slice = buf.slice(offset..offset + size);
queue.write_buffer(&buf, offset, vec_to_bytes(&data));
slice
}
}
// Here's the calling code.
#[derive(Clone, Copy)]
struct Mesh<'a> {
vertices: wgpu::BufferSlice<'a>,
vertex_count: u64,
indices: wgpu::BufferSlice<'a>,
index_count: u64,
}
impl<'a> Mesh<'a> {
pub fn from_vertices(vertices: Vec<standard::Vertex>, indices: Vec<u32>, pool: &'a mut BufferPool, device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
let idx_loc = pool.load_data(&indices, device, queue);
let vtx_loc = pool.load_data(&vertices, device, queue);
Self {
index_count : indices.len() as u64,
indices : idx_loc,
vertex_count: vertices.len() as u64,
vertices: vtx_loc,
}
}
}
And obviously the borrow checker isn't happy because:
wgpu::BufferSlice
holds a reference to awgpu::Buffer
BufferPool::load_data()
takes a mutable reference and I call it twice in a row to upload my stuff
To me, from now on the BufferSlice
will be read only, so I don't need to hold a mutable reference to the pool. But I need to give it one when loading data to grow it if needed.
Possible solutions:
- Just give an ID in the array of Buffers: could work, but then at draw time I'd need to convert it all back to a
BufferSlice
anyway, so I'd have to pass theBufferPool
to every draw call. And it feels a bit unrusty. - Split your call into two, first a "prepare" then a "send": same deal, a bit dumb to impose this constraint on the caller. And I'll still have multiple borrows when I'll have to upload multiple meshes.
Other issues :
- I have to pass my
wgpu::Device
andwgpu::Queue
to every call, this is a bit dumb to me. Should the pool hold references to theDevice
andQueue
(adds lifetimes everywhere), maybe use anRc::Weak
? (Runtime cost?) - I wish I could return a ref to the last buffer in
BufferPool::maybe_grow
, but then I get double borrows again, how could I handle this cleanly?
I'm still lacking the way to get into the proper mindset. How do you guys go about taking on these tasks? Is there a miracle trait I'm missing? Rc
all the things?
Thank you!!