CUDA / OpenGL互操作性:写入表面对象不会擦除先前的内容

时间:2018-12-23 04:08:08

标签: c++ opengl cuda

我正在尝试使用CUDA内核修改OpenGL纹理,但是遇到一个奇怪的问题,我对surf2Dwrite()的调用似乎与纹理的先前内容混合在一起,如您在图像中所见下面。背面的木质纹理是使用我的CUDA内核修改之前的纹理。预期的输出将仅包括颜色渐变,而不包括其后面的木材纹理。我不明白为什么会发生这种融合。

weird texture blending

可能的问题/误解

我是CUDA和OpenGL的新手。在这里,我将尝试解释导致我编写此代码的思考过程:

  • 我使用cudaArray来访问纹理(而不是浮点数数组),因为我读到读写纹理时,缓存局部性更好。
  • 我使用曲面是因为我在某处读到它是修改cudaArray的唯一方法
  • 我想使用表面对象,我知道这是更新的工作方式。旧方法是使用表面引用。

我的代码可能存在一些我不知道如何检查/测试的问题:

  • 我与图像格式不一致吗?也许我没有在某处指定正确的位数/通道数?也许我应该使用float而不是unsigned char

代码摘要

您可以找到一个full minimum working example in this GitHub Gist。由于所有的活动部件,时间很长,但我将尝试进行总结。我欢迎有关如何缩短MWE的建议。总体结构如下:

  1. 从本地存储的文件创建OpenGL纹理
  2. 使用cudaGraphicsGLRegisterImage()向CUDA注册纹理
  3. 调用cudaGraphicsSubResourceGetMappedArray()以获得代表纹理的cudaArray
  4. 创建一个cudaSurfaceObject_t我可以用来写到cudaArray
  5. 将表面对象传递给内核,该内核使用surf2Dwrite()
  6. 写入纹理
  7. 使用纹理在屏幕上绘制矩形

OpenGL纹理创建

我是OpenGL的新手,所以我以LearnOpenGL tutorials的“纹理”部分为起点。这是我设置纹理的方法(使用图像库stb_image.h

GLuint initTexturesGL(){
    // load texture from file
    int numChannels;
    unsigned char *data = stbi_load("img/container.jpg", &g_imageWidth, &g_imageHeight, &numChannels, 4);
    if(!data){
        std::cerr << "Error:  Failed to load texture image!" << std::endl;
        exit(1);
    }

    // opengl texture
    GLuint textureId;
    glGenTextures(1, &textureId);
    glBindTexture(GL_TEXTURE_2D, textureId);

    // wrapping
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

    // filtering
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // set texture image
    glTexImage2D(
        GL_TEXTURE_2D,    // target
        0,                // mipmap level
        GL_RGBA8,         // internal format (#channels, #bits/channel, ...)
        g_imageWidth,     // width
        g_imageHeight,    // height
        0,                // border (must be zero)
        GL_RGBA,          // format of input image
        GL_UNSIGNED_BYTE, // type
        data              // data
    );
    glGenerateMipmap(GL_TEXTURE_2D);

    // unbind and free image
    glBindTexture(GL_TEXTURE_2D, 0);
    stbi_image_free(data);

    return textureId;
}

CUDA Graphics Interop

调用上面的函数后,我在CUDA中注册纹理:

void initTexturesCuda(GLuint textureId){
    // register texture
    HANDLE(cudaGraphicsGLRegisterImage(
        &g_textureResource,                       // resource
        textureId,                                // image
        GL_TEXTURE_2D,                            // target
        cudaGraphicsRegisterFlagsSurfaceLoadStore // flags
    ));

    // resource description for surface
    memset(&g_resourceDesc, 0, sizeof(g_resourceDesc));
    g_resourceDesc.resType = cudaResourceTypeArray;
}

渲染循环

每帧,我运行以下命令来修改纹理并渲染图像:

while(!glfwWindowShouldClose(window)){
        // -- CUDA --

        // map
        HANDLE(cudaGraphicsMapResources(1, &g_textureResource));


        HANDLE(cudaGraphicsSubResourceGetMappedArray(
            &g_textureArray,   // array through which to access subresource
            g_textureResource, // mapped resource to access
            0,                 // array index
            0                  // mipLevel
        ));

        // create surface object (compute >= 3.0)
        g_resourceDesc.res.array.array = g_textureArray;
        HANDLE(cudaCreateSurfaceObject(&g_surfaceObj, &g_resourceDesc));

        // run kernel
        kernel<<<gridDim, blockDim>>>(g_surfaceObj, g_imageWidth, g_imageHeight);

        // unmap
        HANDLE(cudaGraphicsUnmapResources(1, &g_textureResource));

        // --- OpenGL ---

        // clear
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // use program
        shader.use();

        // triangle
        glBindVertexArray(vao);
        glBindTexture(GL_TEXTURE_2D, textureId);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);

        // glfw:  swap buffers and poll i/o events
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

CUDA内核

实际的CUDA内核如下:

__global__ void kernel(cudaSurfaceObject_t surface, int nx, int ny){
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;
    if(x < nx && y < ny){
        uchar4 data = make_uchar4(x % 255, 
                                  y % 255, 
                                  0, 255);
        surf2Dwrite(data, surface, x * sizeof(uchar4), y);
    }
}

2 个答案:

答案 0 :(得分:5)

如果我理解正确,则首先注册纹理,将其映射一次,为表示映射纹理的数组创建一个表面对象,然后取消映射纹理。在每一帧中,您都再次映射资源,要求提供代表映射纹理的数组,然后完全忽略该帧,并使用为您第一次映射资源时返回的数组创建的表面对象。 From the documentation

  

[…]每次映射array时,在resource中设置的值可能会更改。

每次映射资源时,您都必须创建一个新的表面对象,因为您每次可能会得到一个不同的数组。而且,根据我的经验,实际上您会经常得到不同的人。仅在数组实际更改时才创建一个新的表面对象可能是一件正确的事。该文档似乎允许这样做,但是我从未尝试过,所以我无法确定它是否确实有效……

除此之外:您还可以为纹理生成mipmap。您只覆盖了mip级别0。然后使用带有三线性插值的mipmapping渲染纹理。因此,我的猜测是,您恰好以与mip级别0的分辨率完全不匹配的分辨率渲染纹理,因此,最终将在级别0(您在其中编写)和级别1(其中写入)之间进行插值是从原始纹理生成的)…

答案 1 :(得分:1)

事实证明,问题是我错误地为原始木材纹理生成了 mipmaps ,而我的CUDA内核仅修改了0级mipmap。我注意到的混合是OpenGL在我修改的0级mipmap和较低分辨率的木材纹理之间进行插值的结果。

这是通过禁用mipmap插值获得的正确输出。经验教训!

correct output, no mipmapping