OpenGL RAII-style primitives

Good afternoon, Habra users. I haven’t written for a long time and maybe someone was waiting for articles from me - of course not. Since my free time became a bit more, and my GitHub is completely empty, I decided to write my clone Mein kampf Minecraft. With high probability, I will document this - follow my articles on habr.com . Today I will show how I wrapped OpenGL primitives in the RAII style, if it’s interesting - under cat.

Write in the comments what you would like to read about. Should I write Minecraft from scratch articles? Now let's get started. I will show the Buffer Object as an example . The first thing we will do is make the main calls ( glGenBuffers , glDeleteBuffers ) in a separate class.

Somewhere like this:

class buffer_object
{
public:
    buffer_object() noexcept {
        glGenBuffers(1, m_object);
    }
    
    ~buffer_object() noexcept {
        glDeleteBuffers(1, m_object);
    }
    
    uint32_t get_object() const noexcept {
        return m_object;
    }
private:
    uint32_t m_object;
};

Pros:

  • No need to monitor buffer deletion
  • Enabling the OpenGL header does not stick out (if we transfer the definition of functions to * .cpp)

Minuses:

  • We cannot specify the type of buffer (array, element ...) when binding
  • Cannot create multiple buffers with a single call to glGenBuffers
  • Problems accessing a remote object if the buffer was copied

In order to solve the problem with copying, we simply prohibit it. To do this, create a noncopyable class and inherit it.

Noncopyable class example:

struct noncopyable
{
    noncopyable() = default;
            
    noncopyable(noncopyable&&) = default;
    noncopyable& operator = (noncopyable&&) = default;
            
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator = (const noncopyable&) = delete;
};

Great, minus one problem. We think further, how would we solve the problem with binding ... add the bind / unbind functions:

void bind(buffer_type type) noexcept {
    glBindBuffer(static_cast<GLenum>(type), m_object);
}
    
void unbind() noexcept {
    glBindBuffer(static_cast<GLenum>(type), 0);
}

buffer_type is an enum class, but you can do extern const (in * .hpp), and in (* .cpp) do something like:

const uint32_t array_buffer = GL_ARRAY_BUFFER;

Again, that would not stick out

#include <GL/glew.h>

Pros:

  • No need to monitor buffer deletion
  • Enabling the OpenGL header does not stick out (if we transfer the definition of functions to * .cpp)
  • We can indicate the type of buffer (array, element ...) when binding
  • No copying - no copying problems

Minuses:

  • Cannot create multiple buffers with a single call to glGenBuffers

So, almost everything is super, but we still cannot create several buffers ... Let's fix it.

glGenBuffers can take a pointer to an array and the number of buffers to generate.
We need an array, we could use std :: vector, but we need to allocate memory only once and I would prefer std :: array here, although in the future we will have to do this because of this another level of abstraction.

We rewrite our class to std :: array and add a bit of templates:

constexpr uint8_t default_object_count = 1;
constexpr size_t  default_index        = 0;

template <type::buffer_type type, uint32_t count = default_object_count>
class buffer_object : noncopyable
{
public:
    buffer_object() noexcept {
        glGenBuffers(count, m_object.data());
    }
    
    ~buffer_object() noexcept {
        glDeleteBuffers(count, m_object.data());
    }
    
    template <size_t index = default_index>
    void bind() noexcept
    {
        static_assert(index < count, "index larger than array size");
        glBindBuffer(static_cast<GLenum>(type), m_object[index]);
    }
    
    void unbind() noexcept {
        glBindBuffer(static_cast<GLenum>(type), 0);
    }
    
    buffer_object(buffer_object&&) = default;
    buffer_object& operator = (buffer_object&&) = default;
    
private:
    std::array<uint32_t, count> m_object;
};

And here is what we got.

Pros:

  • No need to monitor buffer deletion
  • We can indicate the type of buffer (array, element ...) when binding
  • No copying - no copying problems
  • We can create several buffers with a single call to glGenBuffers

Minuses:

  • Cannot bind different buffers on the same object
  • Enabling OpenGL Header Sticks Out

Well ... too many cons, what can be done?

You can remove the inclusion of GL / glew.h by adding another level of abstraction in which the OpenGL functions will be called (in any case, this should be done if support for OpenGL + DirectX is planned). Binding with different buffers is a bit more complicated, since we can forget which index with which buffer was banned, as an option, add another array and write the buffer type to it. I have not done this yet; at this stage of development, this is enough for me.

Bonus


For more convenient use, I made a scoped bind class. There he is:

constexpr uint32_t default_bind_index = 0;
template <class T>
class binder : utils::noncopyable
{
public:
      template <uint32_t index = default_bind_index>
      binder(T& ref) : m_ref(ref) { m_ref.template bind<index>(); }
    ~binder()                            { m_ref.unbind();}
        
private:
    T& m_ref;
};

Some may be puzzled by this line:

m_ref.template bind<index>();

If we do not specify the template keyword, then we catch the following error:

binder.hpp: 22: 46: Missing 'template' keyword prior to dependent template name 'bind'
This means that in this case, when T is a dependent type. The compiler does not yet know what type m_ref is. Since there was no binding yet, the compiler processes it purely syntactically, therefore, <is interpreted as an operator less than. To indicate to the compiler that this is, in fact, a specialization of a member function template, you must add the template keyword immediately after the dot operator.

And an example of use:

GLuint VAO;
    
primitive::buffer_object<type::buffer_type::array> vbo;
primitive::buffer_object<type::buffer_type::element> ebo;
    
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
    
{
    primitive::binder bind{vbo};
        
    glBufferData(GL_ARRAY_BUFFER, ...);
        
    glVertexAttribPointer(...);
    glEnableVertexAttribArray(...);
        
    glVertexAttribPointer(...);
    glEnableVertexAttribArray(...);
}
    
{
    primitive::binder bind{ebo};
        
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, ...);
        
    glBindVertexArray(0);
}

This code does not use the object for Vertex Array, because it does not work, I haven’t figured out the reasons yet, but I will figure it out soon xD The

wrappers for Buffer Data and OpenGL calls are not ready yet, the calls are not rewritten to DSA (Direct State Access) .

Thanks to those who read it to the end. I will be very glad to criticism and comments.

All Articles