🔥

Julia上でCUDAとOpenGLの相互運用

2021/04/20に公開

概要

JuliaでccallとCUDA.jlの機能を使ってJuliaだけをあらわにフラグメントシェーディングを行います。
とりあえず動いたので過去の私が欲しかった情報を記事にしておきます。
https://gist.github.com/watosar/471f0f6b20d5dd18753b87c497d9a36d

環境

  • Julia 1.6.0
  • CUDA.jl
  • ModernGL.jl
  • GLFW.jl

詳細

CUDAにはOpenGLをはじめとするグラフィックAPIとの相互運用機能が存在します。リンク
今回このOpenGLとの連携機能を使用します。連携の流れは, OpenGL側でPixelBufferObjectを作り、これをCUDA側で捉えて処理、処理されたbufferを描画となります。

CUDAのapiにはruntimeとdriverの2種類が存在し、上のprogramming guideでもruntime apiが使われていますが、driver apiに同じ名称のapiが存在しています(大体の関数でプレフィックスをcuda -> cuにすれば同じものが該当する)

cuda tool kitは要りません。

CUDAのccallはCUDA.jl内に,OpenGLのccallはModernGL.jl内に定義がありますので自分で書かずにこれを使うのがお手軽です。CUDA.jl内のccallはexportに含まれていないので注意してください。

バッファの用意

pixelbuffer_ref = Ref{Cuint}()
glGenBuffers(1, pixelbuffer_ref)
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelbuffer_ref[])
glBufferData(GL_PIXEL_UNPACK_BUFFER, datasize, C_NULL, GL_DYNAMIC_DRAW)
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0)

pboCUDA_ref = Ref{CUgraphicsResource}()
cuGraphicsGLRegisterBuffer(pboCUDA_ref, pixelbuffer_ref[], cudaGraphicsMapFlagsNone)

draw loop内でのCUDAの処理

cudainterop
cuGraphicsMapResources(1, pboCUDA_ref, C_NULL)
num_bytes_ref = Ref{Csize_t}()
pDevPtr_ref = Ref{CUdeviceptr}()
cuGraphicsResourceGetMappedPointer_v2(pDevPtr_ref, num_bytes_ref, pboCUDA_ref[])
p = Base.unsafe_convert(CuPtr{RGB{UInt8}}, pDevPtr_ref[])
texture = Base.unsafe_wrap(CuArray, p, Int(floor(num_bytes_ref[] / 3)))

threads = (32, 32)
numblocks = (width ÷ 32, height ÷ 32)
t = time()
CUDA.@sync begin
    @cuda threads = threads blocks = numblocks kernel(texture, width, height, t)
end

cuGraphicsUnmapResources(1, pboCUDA_ref, C_NULL)

cuGraphicsResourceGetMappedPointerは利用できません。存在はしていますが、呼ぶとinvalid contentのエラーが発生します。公式のドキュメントのどこにも存在していませんでしたが、cuGraphicsResourceGetMappedPointer_v2が正解です。
ポインターの型を必要なものにコンバートし(RGBは3要素のstructとして定義しました)、unsafe_wrapでCuArrayに変換し、kernelの関数に送ります。

CUDAの部分は以上になりますが、描画には記述が足りません。OpenGLが使える方には自明でしょうが私は困りまくったので以下に書いていきます。

OpenGLはバージョンがいくつもあり、以前のバージョンでの記事がネット上にはわんさかあります。最新は4.6です。過去のapiには使えなくなっているものもあります。
ドキュメントはこれです。

さて、今色情報の詰まったbufferだけが存在しています。これを画面に表示したいです。
ここで用意するのは、バッファを表示するテクスチャ、テクスチャを貼る板ポリ、テクスチャとポリゴンをつなぐためのシェーダーの3つです。

テクスチャの用意

glEnable(GL_TEXTURE_2D)
renderedtexture_ref = Ref{Cuint}()
glGenTextures(1, renderedtexture_ref)
glBindTexture(GL_TEXTURE_2D, renderedtexture_ref[])

glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGB8, width, height)

ここにglTexStorage2Dを使うべきとありましたのでこれを使います。

ポリゴンの用意

vertexarrayid_ref = Ref{Cuint}()
glGenVertexArrays(1, vertexarrayid_ref)
glBindVertexArray(vertexarrayid_ref[])

g_vertex_buffer_data = Vector{Float32}([
    -1.0,  1.0, 0.0,
    -1.0, -1.0, 0.0,
     1.0, -1.0, 0.0,

    -1.0,  1.0, 0.0,
     1.0,  1.0, 0.0,
     1.0,  -1.0, 0.0,
])
glGenBuffers(1, vertexbuffer_ref)
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer_ref[])
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, C_NULL)

g_uv_buffer_data = Vector{Float32}([
    0.0, 1.0,
    0.0, 0.0,
    1.0, 0.0,

    0.0, 1.0,
    1.0, 1.0,
    1.0, 0.0
])
glGenBuffers(1, uvbuffer_ref)
glBindBuffer(GL_ARRAY_BUFFER, uvbuffer_ref[])
glBufferData(GL_ARRAY_BUFFER, sizeof(g_uv_buffer_data), g_uv_buffer_data, GL_STATIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, C_NULL)

最初は頂点バッファオブジェクトの初期化です。必要なのかは知りません。
その後は長方形の頂点、UVの頂点をバッファに作成します。

シェーダーの用意

VertexShaderID = glCreateShader(GL_VERTEX_SHADER)
FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER)
vertcode = """
#version 460 core
layout(location = 0) in vec3 vertexPosition_modelspace;
layout(location = 1) in vec2 vertexUV;
out vec2 UV;
void main(){
    gl_Position.xyz = vertexPosition_modelspace;
    gl_Position.w = 1.0;
    UV = vertexUV;
}
"""
frgcode = """
#version 460 core
in vec2 UV;
layout(location = 0) out vec3 color;
uniform sampler2D tex;
void main(){
    color = texture(tex, UV).rgb;
}
"""
VertexSourcePointer = Ref{Ptr{UInt8}}(pointer(vertcode))
FragmentSourcePointer  = Ref{Ptr{UInt8}}(pointer(frgcode))
GC.@preserve vertcode frgcode begin
    glShaderSource(VertexShaderID, 1, VertexSourcePointer, C_NULL);
    glCompileShader(VertexShaderID)
    checkshadererror(VertexShaderID)

    glShaderSource(FragmentShaderID, 1, FragmentSourcePointer, C_NULL)
    glCompileShader(FragmentShaderID)
    checkshadererror(FragmentShaderID)
end
programid = glCreateProgram()
glAttachShader(programid, VertexShaderID)
glAttachShader(programid, FragmentShaderID)
texID = glGetUniformLocation(programid, "tex")
glLinkProgram(programid)
glDeleteShader(VertexShaderID)
glDeleteShader(FragmentShaderID)
function checkshadererror(shaderid)
    result_ref = Ref{Int32}()
    glGetShaderiv(shaderid, GL_COMPILE_STATUS, result_ref);
    if result_ref != GL_TRUE
        infolength_ref = Ref{Int32}()
        glGetShaderiv(shaderid, GL_INFO_LOG_LENGTH, infolength_ref)
        if infolength_ref[] == 0
            println("shader error but no info")
            return
        end
        errormsg = Vector{UInt8}(undef, infolength_ref[])
        glGetShaderInfoLog(shaderid, infolength_ref[], C_NULL, Ref(errormsg, 1))
        error(unsafe_string(pointer(errormsg)))
    end
end

Vector{UInt8}を文字列にするためにポインターを経由してunsafe_stringで解釈しました。もっといい方法があれば教えてください。

描画

glClear(GL_COLOR_BUFFER_BIT)
glUseProgram(shaderid)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, renderedtexture_ref[])
glUniform1i(texID, 0) 

cudainterop()
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixelbuffer_ref[])
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, C_NULL)
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0)

glEnableVertexAttribArray(0)
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer_ref[])
glVertexAttribPointer(
0,3,GL_FLOAT,GL_FALSE, 0, C_NULL
)
glBindBuffer(GL_ARRAY_BUFFER, C_NULL)

glEnableVertexAttribArray(1)
glBindBuffer(GL_ARRAY_BUFFER, uvbuffer_ref[])
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, C_NULL)
glBindBuffer(GL_ARRAY_BUFFER, C_NULL)

glDrawArrays(GL_TRIANGLES, 0, 6)
glDisableVertexAttribArray(0)
glDisableVertexAttribArray(1)

GLFW.SwapBuffers(window)

関数の呼び出し順序はよくわかりませんがこれで動いたのでこうしておきます。
行っている処理はテクスチャのbind, cudaでバッファに書き込み,glTexSubImage2Dでバッファからテクスチャの上書き,頂点シェーダーでポリゴンの描写とUV情報の受け渡しです。

重要そうなところだけここに書きました。後の部分は最初に貼ったコードを見てください。

まとめ

バカみたいに遅いので普通にシェーダーでやったほうがいいです。

Discussion