Elegant way to make a wgpu::Buffer linear allocator?

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 a wgpu::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 the BufferPool 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 and wgpu::Queue to every call, this is a bit dumb to me. Should the pool hold references to the Device and Queue (adds lifetimes everywhere), maybe use an Rc::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!!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.