stevend12
stevend12

Reputation: 75

C++/OpenGL: VAOs work individually but not together

Trying to implement a basic example of VAOs using C++/OpenGL and I've come across a peculiar problem that I have not found a solution for after searching for a while. My program is supposed to render a texture (CT image) to a square and then draw polygons (using GL_LINE_LOOP) on top to outline specific organs. First, I create a VAO for the CT images at initialization:

// Generates textures for CT images
void TxPlanSys::InitSimCT(SimulationCT &S){
    GLuint VBO[2];
    GLfloat Vertices[] = {
        0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f
    };
    GLfloat UvData[] = {
        0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f
    };
    for(int i = 0; i < 6; i++){
        Vertices[3*i] *= (S.Ny*S.dy);
        Vertices[3*i+1] *= (S.Nz*S.dz);
    }

    // Vertex array object for image surface
    glGenVertexArrays(1,&SimCtVaoID);
    glBindVertexArray(SimCtVaoID);
    glGenBuffers(2,VBO);

    // Vertex buffer
    glBindBuffer(GL_ARRAY_BUFFER,VBO[0]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(Vertices),Vertices,GL_STATIC_DRAW);

    GLuint VertexAttributeID = 0;
    glVertexAttribPointer(VertexAttributeID,3,GL_FLOAT,GL_FALSE,0,0);
    glEnableVertexAttribArray(VertexAttributeID);

    // UV buffer
    glBindBuffer(GL_ARRAY_BUFFER,VBO[1]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(UvData),UvData,GL_STATIC_DRAW);

    GLuint UvAttributeID = 1;
    glVertexAttribPointer(UvAttributeID,2,GL_FLOAT,GL_FALSE,0,0);
    glEnableVertexAttribArray(UvAttributeID);

    // Unbind and clean up
    glBindVertexArray(0);
    glDeleteBuffers(2,VBO);

    // Initialize textures for CT images
    glGenTextures(S.Nx,&CtTextureID);
    for(int n = 0; n < S.Nx; n++){
        CurrentSlice = n;
        MakeImage(S);
        glBindTexture(GL_TEXTURE_2D,CtTextureID+n);
        glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,S.Ny,S.Nz,0,GL_RGB,GL_UNSIGNED_BYTE,MainImage);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    CurrentSlice = 0;
}

Next, I create a VAO for a particular contour set at initialization:

void TxPlanSys::InitContour(Structure * S){
    GLuint ContourVertexBuffer, ColorBuffer;
    float buffer[(const int)(S->PointIndex*3)];

    glGenVertexArrays(1,&ContourVaoID);
    glBindVertexArray(ContourVaoID);

    // Vertex buffer
    glGenBuffers(1,&ContourVertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER,ContourVertexBuffer);
    for(int i = 0; i < S->PointIndex; i++){
        buffer[3*i+2] = 0.0;
        buffer[3*i] = S->SurfacePoints[3*i+1];
        buffer[3*i+1] = S->SurfacePoints[3*i+2];
    }
    glBufferData(GL_ARRAY_BUFFER,sizeof(buffer),buffer,GL_STATIC_DRAW);

    GLuint VertexAttributeID = 0;
    glVertexAttribPointer(VertexAttributeID,3,GL_FLOAT,GL_FALSE,0,0);
    glEnableVertexAttribArray(VertexAttributeID);

    // Unbind and clean up
    glBindVertexArray(0);
    glDeleteBuffers(1,&ContourVertexBuffer);
}

Here's where things get wonky. I have my rendering code below:

static void MainDisplay(){
    // Clear window to black
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Model-View-Projection matrix
    glm::mat4 Projection = glm::perspective((3.1415f*30.0f/180.0f), MyToolkit.AspectRatio, 0.1f, 120.0f);
    glm::mat4 View = glm::lookAt(
        glm::vec3(MyToolkit.CameraPos[0],MyToolkit.CameraPos[1],MyToolkit.CameraPos[2]),
        glm::vec3(MyToolkit.CameraPos[0],MyToolkit.CameraPos[1],0),
        glm::vec3(0,1,0)
    );
    glm::mat4 Model = glm::mat4(1.0f);
    glm::mat4 MVP = Projection * View * Model;

    // Set shader & send MVP matrix to shader
    glUseProgram(TPS.ImageShaderID);
    glUniformMatrix4fv(TPS.ImageMvpID, 1, GL_FALSE, &MVP[0][0]);

    // Draw square for image panel
    glBindVertexArray(TPS.SimCtVaoID);
    glBindTexture(GL_TEXTURE_2D,TPS.CtTextureID+TPS.CurrentSlice);
    glDrawArrays(GL_TRIANGLES,0,6);

    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindVertexArray(0);
    glUseProgram(0);

    // Set shader & send MVP matrix to shader
    glUseProgram(TPS.ContourShaderID);
    glUniformMatrix4fv(TPS.ContourMvpID, 1, GL_FALSE, &MVP[0][0]);

    // Draw contours
    glBindVertexArray(TPS.ContourVaoID);
    glPointSize(3.0);
    glDrawArrays(GL_POINTS,0,9);
    glDrawArrays(GL_LINE_LOOP,0,9);

    // Swap buffers to render
    glutSwapBuffers();
}

When I comment out the glDraw commands for the contour only, the CT image draws just fine. Similarly, when I comment out the glDraw commands for the CT image only, the contour draws just fine. However, when I try to render both of them in the same function call, the contour VAO grabs the vertex buffer from the CT image VAO and renders it instead.

I wish I could insert pictures, but my rep. isn't high enough yet....

After looking in several places and reading up on VAOs I'm pretty stumped. Any help would be greatly appreciated. Here's my comp. info: Ubuntu 14.04, Intel i5 CPU, ATI Radeon graphics, running OpenGL 4.3. Thanks!

Upvotes: 2

Views: 217

Answers (1)

Reto Koradi
Reto Koradi

Reputation: 54592

By deleting VBOs that are still in use, you have stepped into the wonderful world (yes, that's sarcasm) of OpenGL object lifetime. This is a world that, unless you enjoy spending time with spec documents, can be full of surprises. It also involves significant inconsistencies. For example, program and shader objects are managed very differently from other types of objects, like textures and buffers.

The good news is that you can avoid all these pitfalls if you do not delete your objects until you're done using them. Since that's normally fairly easy to manage, I think that it's generally the best policy.

But since you went there, let's try and understand the specific case. The key parts of your code are:

glGenVertexArrays(1, &ContourVaoID);
glBindVertexArray(ContourVaoID);

glGenBuffers(1, &ContourVertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, ContourVertexBuffer);
...
glVertexAttribPointer(...);
...
glBindVertexArray(0);
glDeleteBuffers(1, &ContourVertexBuffer);

You're deleting a VBO that is referenced by a VAO, while the VAO is still used later. This is to some degree ok, but with some caveats.

Object Names vs. Objects

One important aspect to fully understand this is the distinction between object names and objects. When you call:

glGenBuffers(1, &bufId);

you create a buffer name. At this point, no buffer object has been created yet. The actual buffer object is only created the first time you bind the name:

glBindBuffer(GL_ARRAY_BUFFER, bufId);

This demonstrates that the buffer name and the buffer object have different lifetimes.

What the Spec Says

With that understanding, we can see what the spec says about the deletion behavior:

When a buffer, texture, sampler, renderbuffer, query, or sync object is deleted, its name immediately becomes invalid (e.g. is marked unused), but the underlying object will not be deleted until it is no longer in use.

Where "in use" includes being referenced by a container object, like a VAO in this case. So as a first approximation, the VBO is indeed still alive after the call sequence from your code. However, the name (id) is not valid anymore. There's a lengthy paragraph describing the consequences:

Caution should be taken when deleting an object attached to a container object (such as a buffer object attached to a vertex array object, or a renderbuffer or texture attached to a framebuffer object), or a shared object bound in multiple contexts. Following its deletion, the object’s name may be returned by Gen* commands, even though the underlying object state and data may still be referred to by container objects, or in use by contexts other than the one in which the object was deleted. Such a container or other context may continue using the object, and may still contain state identifying its name as being currently bound, until such time as the container object is deleted, the attachment point of the container object is changed to refer to another object, or another attempt to bind or attach the name is made in that context. Since the name is marked unused, binding the name will create a new object with the same name, and attaching the name will generate an error.

I frankly don't completely get the last part about generating an error. But I think it's safe to say that this can result in messy situations. The way I read it, you can essentially end up with 2 objects that had/have the same name, and things get ugly.

Conclusion

At the end of the code above, you would basically be safe to use the VAO that references the buffer you deleted. But when you allocate a new buffer, which you do for your second object, things get much more complicated and problematic.

Unless you like pain, by far the best option is to not delete the VBO at that point. Keep the name around, and delete it at the same time as the VAO(s) that uses it.

BTW: Some of the above is still simplified. For example, if you delete a VBO while the VAO using it is bound, the VBO is automatically unbound from the VAO, and deleted immediately. As I said at the start: Welcome to the wonderful world of OpenGL object lifetime.

Upvotes: 1

Related Questions