Welcome back!

Previously, we have finished abstracting OpenGL vertex data types.

This time, we will create thin wrappers around VBOs and VAOs.

Let’s review what we had previously. We stored the VBO in a simple value:

let mut vbo: gl::types::GLuint = 0;

We created new GL buffer object with glGenBuffers call, for the vbo to receive a new handle to it:

unsafe {
    gl.GenBuffers(1, &mut vbo);
}

We then uploaded data from vertices to the VBO:

unsafe {
    gl.BindBuffer(gl::ARRAY_BUFFER, vbo);
    gl.BufferData(
        gl::ARRAY_BUFFER, // target
        (vertices.len() * std::mem::size_of::<Vertex>()) as gl::types::GLsizeiptr, // size of data in bytes
        vertices.as_ptr() as *const gl::types::GLvoid, // pointer to data
        gl::STATIC_DRAW, // usage
    );
    gl.BindBuffer(gl::ARRAY_BUFFER, 0);
}

Later, we linked this VBO to VAO:

gl.BindVertexArray(vao);
gl.BindBuffer(gl::ARRAY_BUFFER, vbo);

.. So that we could use VAO to draw a triangle (which would pick up the correct VBO from its state):

gl.BindVertexArray(vao);
gl.DrawArrays(
    gl::TRIANGLES, // mode
    0, // starting index in the enabled arrays
    3 // number of indices to be rendered
);

One thing we were not doing: we were not cleaning up the VBO at the end of the program:

unsafe {
    gl.DeleteBuffers(1, &mut vbo);
}

ArrayBuffer Wrapper

A simple struct with a constructor new and a drop implementation will ensure a single creation and a single drop:

(src/render_gl/mod.rs)

pub mod buffer;

(src/render_gl/buffer.rs)

use gl;

pub struct ArrayBuffer {
    vbo: gl::types::GLuint,
}

impl ArrayBuffer {
    pub fn new(gl: &gl::Gl) -> ArrayBuffer {
        let mut vbo: gl::types::GLuint = 0;
        unsafe {
            gl.GenBuffers(1, &mut vbo);
        }

        ArrayBuffer {
            vbo
        }
    }
}

impl Drop for ArrayBuffer {
    fn drop(&mut self) {
        unsafe {
            gl.DeleteBuffers(1, &mut self.vbo);
        }
    }
}

Which would be nice and good, except we can’t access the Gl in drop. We can reuse the same solution we did back in shader program wrapper where we stored a reference-counted Gl inside the shader program struct.

(src/render_gl/buffer.rs)

use gl;

pub struct ArrayBuffer {
    gl: gl::Gl,
    vbo: gl::types::GLuint,
}

impl ArrayBuffer {
    pub fn new(gl: &gl::Gl) -> ArrayBuffer {
        let mut vbo: gl::types::GLuint = 0;
        unsafe {
            gl.GenBuffers(1, &mut vbo);
        }

        ArrayBuffer {
            gl: gl.clone(),
            vbo
        }
    }
}

impl Drop for ArrayBuffer {
    fn drop(&mut self) {
        unsafe {
            self.gl.DeleteBuffers(1, &mut self.vbo);
        }
    }
}

Yep, because of this additional gl field, this wrapper steps a bit outside of zero-cost claim. It may also be a problem if we tried to create millions of small array buffers (which may also be another, bigger problem). Simplest way I can think of to remedy this would be to avoid all the lies and make another struct named ArrayBuffers, plural, which would always generate multiple buffers, but store one reference to Gl for all of them, and match OpenGL API 1:1:

pub struct ArrayBuffers {
    gl: gl::Gl,
    vbo: Vec<gl::types::GLuint>,
}

We are not going to follow this route though - for the sake of keeping the API as simple as possible (but it would not be very different).

Back to the ArrayBuffer then. All that’s left is adding bind and unbind methods:

(src/render_gl/buffer.rs, added)

impl ArrayBuffer {
    ...

    pub fn bind(&self) {
        unsafe {
            self.gl.BindBuffer(gl::ARRAY_BUFFER, self.vbo);
        }
    }

    pub fn unbind(&self) {
        unsafe {
            self.gl.BindBuffer(gl::ARRAY_BUFFER, 0);
        }
    }
    
    // continue here
}

.. As well as a method to upload the data, that works with any slice or Vec:

    pub fn static_draw_data<T>(&self, data: &[T]) {
        unsafe {
            self.gl.BufferData(
                gl::ARRAY_BUFFER, // target
                (data.len() * ::std::mem::size_of::<T>()) as gl::types::GLsizeiptr, // size of data in bytes
                data.as_ptr() as *const gl::types::GLvoid, // pointer to data
                gl::STATIC_DRAW, // usage
            );
        }
    }

Using the Array Buffer

Now we can go back to main.rs and replace VBO initialization with the new one:

(src/main.rs, modified)

use render_gl::buffer;

...


fn run() -> Result<(), failure::Error> {
    ...
    
    let vbo = buffer::ArrayBuffer::new(&gl);
    vbo.bind();
    vbo.static_draw_data(&vertices);
    vbo.unbind();
    
    ...
}

And also use it in the VAO code:

(src/main.rs, modified)

    ...
    
    unsafe {
        gl.BindVertexArray(vao);
        vbo.bind(); // here
    
        Vertex::vertex_attrib_pointers(&gl);
    
        vbo.unbind(); // here
        gl.BindVertexArray(0);
    }
    
    ...

This should run.

Discussion about Buffer

The static_draw_data method could have called bind and unbind.

Yes, but let’s make the API we expose somewhat consistent: if we expose separate low level calls such as bind, let’s not hide secret calls to bind in any other methods.

The VBO is immutable, but we upload data to it!

In Rust, mutable/immutable is not about some ideal concept of mutability, it is about memory safety guarantees. If we are using OpenGL drivers, then all bets are off. So there. Of course, we do try, but in this case the benefits are not very clear. But we may consider &mut self references in cases where they helped to enforce certain API usage.

Vertex Array Object Wrapper

Let’s follow a very similar approach and build VAO wrapper:

(src/render_gl/buffer.rs, added code)

pub struct VertexArray {
    gl: gl::Gl,
    vao: gl::types::GLuint,
}

impl VertexArray {
    pub fn new(gl: &gl::Gl) -> VertexArray {
        let mut vao: gl::types::GLuint = 0;
        unsafe {
            gl.GenVertexArrays(1, &mut vao);
        }

        VertexArray {
            gl: gl.clone(),
            vao
        }
    }
    
    // continue here
}

impl Drop for VertexArray {
    fn drop(&mut self) {
        unsafe {
            self.gl.DeleteVertexArrays(1, &mut self.vao);
        }
    }
}

(src/render_gl/buffer.rs, continued)

.. And, like for the VBO, add bind/unbind methods:

    pub fn bind(&self) {
        unsafe {
            self.gl.BindVertexArray(self.vao);
        }
    }

    pub fn unbind(&self) {
        unsafe {
            self.gl.BindVertexArray(0);
        }
    }

.. And replace the VAO initialization and uses of bind in the main.rs:

(src/main.rs, changed run function)

...
    // set up vertex array object

    let vao = buffer::VertexArray::new(&gl);  // changed

    vao.bind();                               // changed
    vbo.bind();                               // changed
    Vertex::vertex_attrib_pointers(&gl);      // changed
    vbo.unbind();                             // changed
    vao.unbind();                             // changed
...

    // draw triangle

    shader_program.set_used();                
    vao.bind();                               // changed
    unsafe {
        gl.DrawArrays(
            gl::TRIANGLES, // mode
            0, // starting index in the enabled arrays
            3 // number of indices to be rendered
        );
    }

As expected, this should compile and run.

Discussion

We have removed a lot of unsafe code!

Again, we might have created a helper for vertex_attrib_pointers that calls all the necessary binds…

Yes, but that again would hide some details. And maybe sometimes we would like to avoid a call or two to bind or unbind. Let’s leave this as explicit as possible for now.

But now the order of which is destroyed - VAO or VBO, depends on the order the values were declared!

Indeed, this might be an issue. For example, we know that VAO holds a reference to buffer, so we could design an API that reflects that. However, to be able to use the same buffer for multiple VAOs, we would have to make buffer reference-counted.

Thing is, OpenGL drivers are already counting the references. A call to glDeleteBuffers does not mean immediate deallocation, at least not in this case. If a buffer is referenced by VAO, it lives until the VAO is destroyed. It feels kind of wasteful to do this reference-counting twice, so we might just get away without it.

Element Array Buffer Wrapper

gl::ELEMENT_ARRAY_BUFFER works exactly like gl::ARRAY_BUFFER, so to create a rusty wrapper for it we could just copy-paste ArrayBuffer struct into a new ElementArrayBuffer struct.

But I think it’s time for a bit of heavier use of generics and traits, and this seems like a good place to start (and a bit non-standard, as we will see shortly). We could rename ArrayBuffer to generic Buffer<BufferType>, where BufferType would hold either gl::ARRAY_BUFFER or gl::ELEMENT_ARRAY_BUFFER (and other buffer types).

We will face two issues:

  • Rust does not yet have support for generics over int values (which are very common in C++);
  • In Rust, generic parameters have to be used.

But both of these issues have decent solutions that don’t impose additional runtime cost. Let’s begin.

Generic Buffer

Let’s rename render_gl::ArrayBuffer to render_gl::Buffer.

Then, let’s make render_gl::Buffer generic. As mentioned, const generics are not yet in the language. However, our type can be generic over another type that implements a trait. And this trait can have a constant BUFFER_TYPE value:

(src/render_gl/buffer.rs, add code)

pub trait BufferType {
    const BUFFER_TYPE: gl::types::GLuint;
}

Then, when making Buffer<B> generic over the B type, we can specify this trait as trait bound for the B with where B: BufferType syntax:

(src/render_gl/buffer.rs, replace struct ArrayBuffer with impl)

pub struct Buffer<B> where B: BufferType {
    gl: gl::Gl,
    vbo: gl::types::GLuint,
}

impl<B> Buffer<B> where B: BufferType {
    pub fn new(gl: &gl::Gl) -> Buffer<B> {
        let mut vbo: gl::types::GLuint = 0;
        unsafe {
            gl.GenBuffers(1, &mut vbo);
        }

        Buffer {
            gl: gl.clone(),
            vbo,
        }
    }

    pub fn bind(&self) {
        unsafe {
            self.gl.BindBuffer(B::BUFFER_TYPE, self.vbo);
        }
    }

    pub fn unbind(&self) {
        unsafe {
            self.gl.BindBuffer(B::BUFFER_TYPE, 0);
        }
    }

    pub fn static_draw_data<T>(&self, data: &[T]) {
        unsafe {
            self.gl.BufferData(
                gl::ARRAY_BUFFER, // target
                (data.len() * ::std::mem::size_of::<T>()) as gl::types::GLsizeiptr, // size of data in bytes
                data.as_ptr() as *const gl::types::GLvoid, // pointer to data
                gl::STATIC_DRAW, // usage
            );
        }
    }
}

impl<B> Drop for Buffer<B> where B: BufferType {
    fn drop(&mut self) {
        unsafe {
            self.gl.DeleteBuffers(1, &mut self.vbo);
        }
    }
}

However, this won’t compile as-is:


error[E0392]: parameter `B` is never used
  --> lesson-12\src\render_gl\buffer.rs:17:19
   |
17 | pub struct Buffer<B> where B: BufferType {
   |                   ^ unused type parameter
   |
   = help: consider removing `B` or using a marker such as `std::marker::PhantomData`
   

What is this PhantomData?

When we create structures that act as if they contain a value, but actually don’t, we have to tell Rust what we are faking. We use this built-in compiler type PhantomData to do exactly that.

In this case, we are acting as if B is a value. In particular, if the Buffer is dropped, B should be dropped too. In other words, if we would add it to the struct, it would look like this:

(example)

pub struct Buffer<B> where B: BufferType {
    ...
    foo: B, // added value
}

To explain this to compiler, we add a _marker field of the same type, but wrap it in the PhantomData:

(src/render_gl/buffer.rs, modified Buffer struct)

pub struct Buffer<B> where B: BufferType {
    gl: gl::Gl,
    vbo: gl::types::GLuint,
    _marker: ::std::marker::PhantomData<B>,
}

In our use case, this does not make any difference. But in some cases, we may need to implement internals of a struct using raw pointers without actually holding a value, and it would matter. You can read more on that in the PhantomData documentation.

PhantomData is zero-sized, so it does not take additional space in the struct.

We also need to modify new function to create this PhantomData. That’s easy, and the compiler can infer the generic PhantomData type:

(src/render_gl/buffer.rs, modified Buffer::new function)

    pub fn new(gl: &gl::Gl) -> Buffer<B> {
        let mut vbo: gl::types::GLuint = 0;
        unsafe {
            gl.GenBuffers(1, &mut vbo);
        }

        Buffer {
            gl: gl.clone(),
            vbo,
            _marker: ::std::marker::PhantomData, // added
        }
    }

Now we just need something to implement BufferType trait and return gl::ARRAY_BUFFER or gl::ELEMENT_ARRAY_BUFFER.

For that, we can use zero-sized structs:

(src/render_gl/buffer.rs, added)

pub struct BufferTypeArray;
impl BufferType for BufferTypeArray {
    const BUFFER_TYPE: gl::types::GLuint = gl::ARRAY_BUFFER;
}

pub struct BufferTypeElementArray;
impl BufferType for BufferTypeElementArray {
    const BUFFER_TYPE: gl::types::GLuint = gl::ELEMENT_ARRAY_BUFFER;
}

With this done, we can now initialize our buffer with this call:

(example)

Buffer::<BufferTypeArray>::new(&gl)

But we can do a bit better and shorten Buffer::<BufferTypeArray> type by defining some type aliases:

(src/render_gl/buffer.rs, added)

pub type ArrayBuffer = Buffer<BufferTypeArray>;
pub type ElementArrayBuffer = Buffer<BufferTypeElementArray>;

Now, we can leave the same code in main as it was before making the array generic. Additionally, we have a new ElementArrayBuffer type ready for use in a later lesson.

But the next time, we will finish cleaning up our run function.

Full source code is available on github.