🐕

OpenGL: DSAとRAIIを使ったモダンなオブジェクト管理

2023/06/28に公開

OpenGL4.5でDirect State Access(DSA)という便利な機能が追加されました。

従来OpenGLでバッファなどのオブジェクトを操作するには、グローバルコンテキストに一回バインドしてからでないと操作が出来ませんでした。このような手続きは直感的でなく、コード量の増加にも繋がって可読性を下げる要因の1つになっていると思います。

DSAを使うとこの面倒なバインド手続きから解放され、グローバルコンテキストを経由せずにバッファやテクスチャなどのオブジェクトを操作できるようになります。

DSAの使い方

ここではバッファを例に使い方を見ていきます。

DSAによってバッファに関連する以下の関数が追加されています。

従来の方法ではバッファを作成して頂点データを渡すには以下のように記述する必要がありました。

// バッファの作成
GLuint buffer;
glGenBuffers(1, &buffer);

// バインド
glBindBuffer(GL_ARRAY_BUFFER, buffer); 

// 頂点データの送信
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

DSAを使うと以下のように記述することが出来ます。

# バッファの作成
GLuint buffer;
glCreateBuffers(1, &buffer);

# 頂点データの送信
glNamedBufferData(buffer, sizeof(vertices), vertices, GL_STATIC_DRAW);

バインドの部分が消えてスッキリしました。また、グローバルコンテキストを経由せずに直接bufferに対してデータを送信することが出来るので、コードが分かりやすくなっています。

DSAはバッファに対してだけではなく、テクスチャやフレームバッファに対しても使うことが出来ます。以下はテクスチャの例です。グローバルコンテキストへのバインドが一回も出てきていないのが分かると思います。

// テクスチャの作成
GLuint texture;
glCreateTextures(GL_TEXTURE_2D, 1, &texture);

// パラメーターの設定
glTextureParameteri(texture, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTextureParameteri(texture, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

// テクスチャデータの送信
glTextureStorage2D(texture, 1, GL_RGB8, width, height);
glTextureSubImage2D(texture, 0, 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, image);

// テクスチャをテクスチャユニット(0番)にバインド
glBindTextureUnit(0, texture);

その他にもDSAに関連する様々なAPIが追加されています。詳細は仕様書を見てください。

OpenGLを使ってアプリケーションを作る際には、生のOpenGLの関数の呼び出しを色んな所にベタ書きするのではなく、何らかのラッパーを経由してOpenGLのオブジェクトを操作することが多いと思います。DSAを使うとオブジェクトを操作するのにグローバルコンテキストを経由しなくて良くなるので、そのようなラッパーが更に作りやすくなります。

RAIIとの組み合わせ

Resource Acquisition Is Initialization(RAII)はクラスのコンストラクタでリソースを確保し、デストラクタでリソースを解放するプログラミングテクニックです。RAIIを使うとクラスのインスタンスが1つのリソースに対応します。リソースの確保と解放をユーザーが明示的に行う必要がなくなるので、リソース管理が楽になります。

OpenGLのバッファの場合だと、バッファを作る部分がリソースの確保、バッファを削除する部分がリソースの解放に対応します。

DSAとRAIIを組み合わせることで、ラッパーを綺麗に作ることが出来ます。以下はバッファのラッパークラスの例です。

class Buffer
{
   private:
    GLuint buffer;
    uint32_t size;

    // リソースの解放
    void release()
    {
        if(buffer)
        {
          glDeleteBuffers(1, &buffer);
	  buffer = 0;
        }
    }

   public:
    Buffer() : buffer{0}, size{0}
    {
        glCreateBuffers(1, &buffer);
    }
    Buffer(const Buffer& buffer) = delete;
    Buffer(Buffer&& other) : buffer(other.buffer), size(other.size)
    {
        other.buffer = 0;
    }
    ~Buffer() { release(); }

    Buffer& operator=(const Buffer& buffer) = delete;
    Buffer& operator=(Buffer&& other)
    {
        if(this != &other) {
          release();
	
	  buffer = other.buffer;
	  size = other.size;
	
	  other.buffer = 0;
	  other.size = 0;
        }
        return *this;
    }
    
    GLuint getName() const { return buffer; }
    uint32_t getLength() const { return size; }

    // バッファにデータを送信する
    template <typename T>
    void sendData(const T* data, uint32_t n, GLenum usage)
    {
        glNamedBufferData(buffer, sizeof(T) * n, data, usage);
        size = n;
    }

    // SSBOにバインドする
    void bindToShaderStorageBuffer(GLuint binding_point_index) const
    {
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, binding_point_index, buffer);
    }
};

RAIIではクラスが管理しているリソースの二重確保や解放忘れを防ぐために、コピーコンストラクタを削除し、ムーブだけ許容するようにします。

ラッパークラスを使うとDSAの例で見たバッファの作成は以下のように書けます。

// バッファの作成とデータの送信
Buffer buffer;
buffer.sendData(vertices, 1, GL_STATIC_DRAW);

// バッファをSSBO(0番)にバインド
buffer.bindToShaderStorageBuffer(0);

さらにシンプルになりました。bufferのデストラクタが呼ばれると管理しているバッファも自動的に解放されるので、リソース管理も楽になりました。

他にもDSAやRAIIを使った例を見たい方は

などを覗いてみてください。

Discussion