RAII wrapper for OpenGL objects

2019-01-13 17:23发布

问题:

I want to write a simple RAII wrapper for OpenGL objects (textures, frame buffers, etc.) I have noticed, that all glGen* and glDelete* functions share the same signature, so my first attempt was like this:

typedef void (__stdcall *GLGenFunction)(GLsizei, GLuint *);
typedef void (__stdcall *GLDelFunction)(GLsizei, const GLuint *);

template <GLGenFunction glGenFunction, GLDelFunction glDelFunction>
class GLObject
{
    GLuint m_name;
public:
    GLObject() 
    {  
        glGenFunction(1, &m_name);
    }

    ~GLObject()
    {
        glDelFunction(1, &m_name);
    }

    GLuint getName() {return m_name;}
};

typedef GLObject<glGenTextures, glDeleteTextures> GLTexture;

It works fine for textures, but fails for frame buffers: glGenFramebuffers and glDeleteFramebuffers function addresses are not known at compile time, and cannot be used as template arguments. So I made second version:

class GLObjectBase
{
    GLuint m_name;
    GLDelFunction m_delFunction;

public:
    GLObjectBase(GLGenFunction genFunc, GLDelFunction delFunction)
        : m_delFunction(delFunction)
    {
        genFunc(1, &m_name);
    }

    GLuint getName()
    {
        return m_name;
    }

protected:
    ~GLObjectBase()
    {
        m_delFunction(1, &m_name);
    }
};

class GLFrameBuffer : public GLObjectBase
{
public:
    GLFrameBuffer() : GLObjectBase(glGenFramebuffers, glDeleteFramebuffers) {}
};

But I don't like it since I have to store del function pointer in each instance that will not change at run-time.

How do I make wrapper class that stores only object name in each instance without resorting to create a bunch of almost copy-pasted classes?

I could do something like this:

template <int N>
class GLObject2
{
    GLuint m_name;
    static GLDelFunction glDelFunction;
public:
    GLObject2(GLGenFunction genFunction, GLDelFunction delFunc)
    {  
        genFunction(1, &m_name);
        if ( glDelFunction == nullptr )
            glDelFunction = delFunc;
        ASSERT(glDelFunction == delFunc);
    }

    GLuint getName() {return m_name;}

protected:
    ~GLObject2()
    {
        glDelFunction(1, &m_name);
    }
};

template <int N>
GLDelFunction GLObject2<N>::glDelFunction = nullptr;

class GLTexture: public GLObject2<1>
{
public:
    GLTexture(): GLObject2<1>(glGenTextures, glDeleteTextures) {}
};

class GLRenderBuffer: public GLObject2<2>
{
public:
    GLRenderBuffer(): GLObject2<2>(glGenRenderbuffers, glDeleteRenderbuffers) {}
};

Can anyone suggest more elegant solution?

回答1:

Really, you're thinking about this like a C programmer. You're using C++, so solve it the way a C++ programmer would. With a traits class:

struct VertexArrayObjectTraits
{
  typedef GLuint value_type;
  static value_type Create();
  static void Destroy(value_type);
};

Like a proper C++ traits class, we have each object declare it's own value_type. This will allow you to adapt it to OpenGL objects that don't use GLuints, like sync objects (though the Create/Destroy interface wouldn't be good for them anyway, so you probably shouldn't bother).

So you write one traits class for each type of OpenGL object. Your Create and Destroy functions will forward the calls on to the C API appropriately.

After doing that, all you need is a RAII-wrapper around those interfaces:

template<typename T>
class OpenGLObject
{
public:
  OpenGLObject() : m_obj(T::Create()) {}
  ~OpenGLObject() {T::Destroy(m_obj);}

  operator typename T::value_type() {return m_obj;}

private:
  typename T::value_type m_obj;
};

An OpenGLObject<VertexArrayObjectTraits> would hold a VAO.



回答2:

Why reinvent the wheel? There is a neat solution using std::unique_ptr, which already provides the needed functionality, so you need to only write the traits (!):

template<void (*func)(GLuint)>
struct gl_object_deleter {
    struct pointer { // I wish we could inherit from GLuint...
        GLuint x;
        pointer(std::nullptr_t = nullptr) : x(0) {}
        pointer(GLuint x) : x(x) {}
        operator GLuint() const { return x; }
        friend bool operator == (pointer x, pointer y) { return x.x == y.x; }
        friend bool operator != (pointer x, pointer y) { return x.x != y.x; }
    };
    void operator()(GLuint p) const { func(p); }
};

void delete_texture(GLuint p) { glDeleteTextures(1, &p); }
void delete_shader(GLuint p) { glDeleteShader(p); }
// ...
typedef std::unique_ptr<void, gl_object_deleter<delete_texture>> GLtexture;
typedef std::unique_ptr<void, gl_object_deleter<delete_shader>> GLshader;
// ...

Most the Create* functions return an array through their argument, which is inconvenient when you allocate your objects one-by-one. It is possible to define a set of creation routines for single instances:

GLuint glCreateTextureSN(GLenum target) { GLuint ret; glCreateTextures(target, 1, &ret); return ret; }
GLuint glCreateBufferSN() { GLuint ret; glCreateBuffers(1, &ret); return ret; }
// ...

Some OpenGL functions, like glCreateShader can be used directly. Now we can use it as follows:

GLtexture tex(glCreateTextureSN(GL_TEXTURE_2D));
glTextureParameteri(tex.get(), GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
// ...
GLtexture tex2 = std::move(tex); // we can move
tex2.reset(); // delete
return tex2; // return...

One downside is that you cannot define an implicit cast to GLuint, so you must call get() explicitly. But, on a second thought, preventing an accidental cast to GLuint is not such a bad thing.