Encapsuler des objets OpenGL avec C++ RAII

Exemples de différentes façons de faire fonctionner des objets OpenGL avec C++ RAII.

L’encapsulation RAII des objets OpenGL présente des dangers. Le plus inévitable est que les objets OpenGL sont associés au contexte OpenGL qui les a créés. Ainsi, la destruction d’un objet C++ RAII doit être effectuée dans un contexte OpenGL qui partage la propriété de l’objet OpenGL géré par cet objet C++.

Cela signifie également que si tous les contextes qui possèdent l’objet sont détruits, alors tous les objets OpenGL encapsulés RAII existants essaieront de détruire les objets qui n’existent plus.

Vous devez prendre des mesures manuelles pour traiter les problèmes de contexte comme celui-ci.

En C++11 et versions ultérieures

C++11 offre des outils qui améliorent la fonctionnalité des objets OpenGL encapsulés en RAII. Sans les fonctionnalités C++11 telles que la sémantique de déplacement, ces objets devraient être alloués dynamiquement si vous souhaitez les transmettre, car ils ne peuvent pas être copiés. La prise en charge du déplacement permet de les transmettre dans les deux sens comme des valeurs normales, mais pas en les copiant :

class BufferObject
{
public:
    BufferObject(GLenum target, GLsizeiptr size, const void *data, GLenum usage)
    {
        glGenBuffers(1, &object_);
        glBindBuffer(target, object_);
        glBufferData(target, size, data, usage);
        glBindBuffer(target, 0);
    }
    
    //Cannot be copied.
    BufferObject(const BufferObject &) = delete;
    BufferObject &operator=(const BufferObject &) = delete;
    
    //Can be moved
    BufferObject(BufferObject &&other) noexcept : object_(other.Release())
    {}
    
    //Self-assignment is OK with this implementation.
    BufferObject &operator=(BufferObject &&other) noexcept
    {
        Reset(other.Release());
    }
    
    //Destroys the old buffer and claims ownership of a new buffer object.
    //It's OK to call glDeleteBuffers on buffer object 0.
    GLuint Reset(GLuint object = 0)
    {
        glDeleteBuffers(1, &object_);
        object_ = object;
    }
    
    //Relinquishes ownership of the object without destroying it
    GLuint Release()
    {
        GLuint ret = object_;
        object_ = 0;
        return ret;
    }    
    
    ~BufferObject()
    {
        Reset();
    }
    
    //Accessors and manipulators
    void Bind(GLenum target) const {glBindBuffer(target, object_);}
    GLuint GetObject() const {return object_;}

private:
    GLuint object_;
};

Un tel type peut être renvoyé par une fonction :

BufferObject CreateStaticBuffer(GLsizeiptr byteSize) {return BufferObject(GL_ARRAY_BUFFER, byteSize, nullptr, GL_STATIC_DRAW);}

Ce qui vous permet de les stocker dans vos propres types (implicitement de déplacement uniquement) :

struct Mesh
{
public:
private:
    //Default member initializer.
    BufferObject buff_ = CreateStaticBuffer(someSize);
};

Une classe de classeur à portée peut également avoir une sémantique de déplacement, permettant ainsi au classeur d’être renvoyé à partir de fonctions et stocké dans des conteneurs de bibliothèque standard C++ :

class BindBuffer
{
public:
    BindBuffer(GLenum target, const BufferObject &buff) : target_(target)
    {
        buff.Bind(target_);
    }

    //Non-copyable.
    BindBuffer(const BindBuffer &) = delete;
    BindBuffer &operator=(const BindBuffer &) = delete;
    
    //Move-constructible.
    BindBuffer(BindBuffer &&other) noexcept : target_(other.target_)
    {
        other.target_ = 0;
    }
    
    //Not move-assignable.
    BindBuffer &operator=(BindBuffer &&) = delete;
    
    ~BindBuffer()
    {
        //Only unbind if not moved from.
        if(target_)
            glBindBuffer(target_, 0);
    }
    
private:
    GLenum target_;
};

Notez que l’objet est constructible par déplacement mais non assignable par déplacement. L’idée avec ceci est d’empêcher la reliure d’une liaison de tampon étendue. Une fois qu’il est défini, la seule chose qui peut le désactiver est d’être déplacé.

En C++98/03

L’encapsulation d’un objet OpenGL en C++ 98/03 nécessite le respect de la règle C++ de 3. Cela signifie l’ajout d’un constructeur de copie, d’un opérateur d’affectation de copie et d’un destructeur.

Cependant, les constructeurs de copie doivent logiquement copier l’objet. Et copier un objet OpenGL est une entreprise non triviale. Tout aussi important, c’est presque certainement quelque chose que l’utilisateur ne souhaite pas faire.

Nous allons donc à la place rendre l’objet non copiable :

class BufferObject
{
public:
    BufferObject(GLenum target, GLsizeiptr size, const void *data, GLenum usage)
    {
        glGenBuffers(1, &object_);
        glBindBuffer(target, object_);
        glBufferData(target, size, data, usage);
        glBindBuffer(target, 0);
    }
    
    ~BufferObject()
    {
        glDeleteBuffers(1, &object_);
    }
    
    //Accessors and manipulators
    void Bind(GLenum target) const {glBindBuffer(target, object_);}
    GLuint GetObject() const {return object_;}

private:
    GLuint object_;
    
    //Prototypes, but no implementation.
    BufferObject(const BufferObject &);
    BufferObject &operator=(const BufferObject &);
};

Le constructeur créera l’objet et initialisera les données de l’objet tampon. Le destructeur détruira l’objet. En déclarant le constructeur de copie/l’affectation sans les définir, l’éditeur de liens donnera une erreur si un code tente de les appeler. Et en les déclarant privés, seuls les membres de BufferObject pourront même les appeler.

Notez que BufferObject ne conserve pas la target transmise au constructeur. En effet, un objet tampon OpenGL peut être utilisé avec n’importe quelle cible, pas seulement celle avec laquelle il a été initialement créé. Ceci est différent des objets de texture, qui [doivent toujours être liés à la cible avec laquelle ils ont été initialement créés.] [1]

Étant donné qu’OpenGL dépend beaucoup de la liaison d’objets au contexte à diverses fins, il est souvent utile d’avoir également une liaison d’objet de type RAII. Étant donné que différents objets ont des besoins de liaison différents (certains ont des cibles, d’autres non), nous devons en implémenter un pour chaque objet individuellement.

class BindBuffer
{
public:
    BindBuffer(GLenum target, const BufferObject &buff) : target_(target)
    {
        buff.Bind(target_);
    }
    
    ~BindBuffer()
    {
        glBindBuffer(target_, 0);
    }
    
private:
    GLenum target_;

    //Also non-copyable.
    BindBuffer(const BindBuffer &);
    BindBuffer &operator=(const BindBuffer &);
};

BindBuffer n’est pas copiable, car le copier n’a aucun sens. Notez qu’il ne conserve pas l’accès au BufferObject qu’il lie. C’est parce que c’est inutile.

[1] : https://www.opengl.org/wiki/Texture#Texture_Objects