Tags: c++ opengl opengl_dsa mesa graphics
Or Why The GL Bind Model Sucks
Mesa (the open source GL-like library) just recently added support for GL_ARB_direct_state_access which is huge for me (both AMD and Nvidia support it already). While we’re still many years away from being able to assume we have it, it finally makes GL close to what it should always have been. To explain that though, we’ll need to explain how GL currently works.
For a simple program to explain with, we’ll use a simple image viewer. Given an image, it displays the image : pretty straightforward.
In GL you want to draw something, so you look up the draw command. In this case we use glDrawElements.
void glDrawElements (GLenum mode, GLsizei count, GLenum type, GLvoid* indices);
This function takes a mode (such as drawing triangles or lines), the number of triangles to draw, the type of indices (unsigned int, byte, etc), and a set of indices. But we’re missing a ton of stuff : where are the vertexes to draw? Where is the color or texture? In GL all of these are through bindings - if something is bound to a specific bind point, it’ll be used. So for example, if you bind a Vertex Buffer Object (VBO) to GL_ARRAY_BUFFER, it’ll use it for verticies. If you bind a texture to GL_TEXTURE0, it’ll be handed off for drawing. If you bind a shader program, it’ll be used for drawing, and so on. The set of all bound objects becomes the global GL state. So in our example, we will have all of the following objects bound before we draw:
- VBO - Vertex Buffer Object - Contains the set of vertexes to draw and its data (4 verts for the square, each with a position and a texture coordinate)
- IBO - Index Buffer Object - Contains the indexes to draw (2 triangles, so 6 items)
- VAO - Vertex Array Object - Contains how to read the VBO.
- Texture - The image to draw
- Shader Program - The GPU program to use for drawing. In this case its basically just takes the points from the texture and render them.
- UBO - Uniform Buffer Object - Data for the shader program such as its model view matrix and/or projection.
The key thing to take from this is if any of those objects are not bound correctly, it won’t work. Generally it just wont render, so you’ll get a black screen and have the fun of spending hours debugging it. The problem though gets worse cause of the next thing: in GL to use an object, you have to bind it.
If you’ve ever worked with objects in Java or C++, it’d be like if instead of writing:
str = createString(); str2 = createString(); str.append (“Hello”); str2.append (“World”); str.display();
you had to do:
str = createString(); str2 = createString(); setCurrentString (str); appendCurrentString (“Hello”); setCurrentString (str2); appendCurrentString (“World”); setCurrentString (str); displayCurrentString(); appendCurrentString (“World”);
You can see this gets very tedious really quick, not to mention prevents threading and optimizations. And as soon as another function wants to use a string, they’ve got to be careful about who binds what. If you call a function that happens to bind a string when you didn’t expect, your code runs off the rails. The only way I’ve personally been able to keep it all straight is a huge number of assertions verifying that the OpenGL bind state is what I expect it to be. And that’s not even including fun things like a Vertex Array Object (VAO) which when you bind it, also adjusts the IBO binding, along with all of the vertex attribute bindings. Except if its from Apple’s VAO extension, which has different rules. Quickly becomes spaghetti trying to unravel it all, and due to GL’s nature, any extra bind costs you performance.
So tl;dr, OpenGL’s bind model is ok for drawing, but its very easy to mess up by accident and is very inefficient. So how do we fix it without rewriting all of OpenGL? Enter GL_ARB_direct_state_access and GL 4.5.
This is a feature of GL 4.5 which also has an equivalent extension that creates an alternate version of most of GL’s object functions. Instead of having to bind something, you just give it the identifiers instead. The other major thing it does is it adds Create functions that actually create things instead of just reserving a number and creating on bind (another weird rule of GL). So for example, in old code, to create a VBO and upload data to it, you would:
GLuint openGLID; glGenBuffers (1, &openGLID); // creates an ID, but doesnt create the buffer glBindBuffer (GL_ARRAY_BUFFER, openGLID); // binds it to GL_ARRAY_BUFFER and creates the buffer since it didnt exist yet. Previous binding to GL_ARRAY_BUFFER is lost. glBufferData (GL_ARRAY_BUFFER, data.size(), data.data(), GL_DYNAMIC_DRAW); // upload the data to the buffer // openGLID is still bound at the end of this, and anyone calling us has to realize that. Or you have to do a dance to rebind the old value.
When using DSA, you can direct access the buffer via its ID instead of having to use GL_ARRAY_BUFFER. This ends up like:
GLuint openGLID; glCreateBuffers (1, &openGLID); // creates the buffer glNamedBufferData (openGLID, data.size(), data.data(), GL_DYNAMIC_DRAW); // uploads data to the buffer // we didn’t mess with GL_ARRAY_BUFFER, or any other global state. Any caller of us doesnt need to care at all what we do.
On top of being more object oriented, the global state doesn’t change, you don’t have a random binding point just to be able to use an object, and its something the driver could thread or optimize a lot more. The only downside at the moment is it requires GL 4.5 (which is relatively new), or support for the extension (modern AMD and Nvidia cards support it, Mesa just added support so it’ll filter down to users). Its not common place enough to require yet, and in my case I support GLES which I don’t think has an equivalent.
Just for comparison, most other modern graphics APIs don’t have this problem by being more object oriented to start with. DirectX and Metal both do everything directly with objects, so the only bindings are members on D3DAdapter and similar. To use an object, you just use an object. Mantle and Vulkan are C like, but its still directly object oriented without the concept of a global bind state. Since threaded rendering is a major goal, object access is required to make sure two threads don’t try to stomp on each other when they Bind() something. OpenGL is pretty much the last holdout here : originally the Long Peaks rewrite was supposed to fix this for 3.0, but for various reasons it took till 4.5.
DSA isn’t a huge change, but for anyone who’s had to debug bindings before, its a huge win. Currently i have many many assertions which all they do is make sure the bindings are what they should be, which can all be removed once I can depend on DSA. At the moment, I’m adding support to my engine to help debugging and prepare for the time binding can be removed, but unfortunately binding has to remanin for the foreseable future. But with DSA being added to Mesa, the clock is ticking.