Let’s do OpenGL archeology

Modern OpenGL is not a sane API from a design standpoint: the gigantic global state leaks, you have to bind objects before you edit them and there are special cases everywhere (GL_FRAMEBUFFER anyone?). Also all the objects are GLuints. Please do spend days trying to make a type-safe wrapper before giving up. Flips the table.

For someone like me who started with OpenGL 3 and WebGL, all of that seems gratuitous. Obviously the specification editors didn’t one day decide to write an API you need a law degree to understand and use. In this post we will try to understand why? We will focus on one of the biggest pain points that is the combination of bind-to-edit and glActiveTexture that forces you to bind a texture to change its filtering mode, and results in silently updating the active shader texture behind your back.

Please note that when OpenGL first came out, computers were barely “boxes that make images” for me, so I don’t have insider information on its design. Instead I will look at the early OpenGL specifications and try to recreate the decisions that led to various OpenGL weirdness. If you happened to participate in these design decisions and notice something wrong or incomplete below, please contact me at corentin@wallez.net or @DaKangz.

OpenGL 1.0, fixed function and glTexImage2D

The OpenGL 1.0 specification can still be found in the OpenGL registry. It was purely fixed-function with zero programmability; all you could do was turn hardware knobs and submit vertices to be transformed by geometry engines, then rasterized by raster engines (at least on SGI hardware). This was a great API because it was open and gave direct access to the hardware contrary to the scenegraph-based PHIGS alternative.

In the specification, we can see familiar state-setting functions like glEnable, glBlend, glScissor and glClearColor. To keep backwards compatibility and consistency, future iterations of OpenGL will keep this style of using immediate state-setting function calls. Nowadays Vulkan, DX12 and Metal (later called modern APIs) use the more efficient compiled pipeline state objects but these cost precious transistors to store and address. It would have been too complex for 1993’s GPUs especially since they were made of several independent chips. On the other hand the OpenGL function calls can be streamed through the hardware to update registers efficiently.

In OpenGL 1.0 texturing was already available through the glTexImage2D and the glTexcoord2f calls and could be used to texture triangles like in this snippet:

// Set up the fixed function state
glLoadIdentity();
glTranslatef(-1.5f,0.0f,-6.0f);
glEnable(GL_BLEND);

// Use a texture
glEnable(GL_TEXTURE_2D)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, myTextureData);
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);

// Draw a textured triangle
glBegin(GL_TRIANGLES)
    glTexCoord2f(0.0f, 0.0f)
    glVertex3f(-1.0f, -1.0f,  1.0f);
    glTexCoord2f(1.0f, 0.0f);
    glVertex3f( 1.0f, -1.0f,  1.0f);
    glTexCoord2f(1.0f, 1.0f);
    glVertex3f( 1.0f,  1.0f,  1.0f);
    glTexCoord2f(0.0f, 1.0f);
    glVertex3f(-1.0f,  1.0f,  1.0f);
glEnd()

What is interesting here is the lack of glBindTexture call, and indeed it didn’t exist in OpenGL 1.0. Only one texture was supported, and the way to change the texture was to re-specify it completely. It seems wasteful but the cost could be reduced by using display lists, lists of OpenGL commands that were cached directly on the hardware [1]. It isn’t clear why texturing worked this way, it is possible that SGI hardware didn’t support more than one resident texture or that most applications used few textures per frame, if any.

OpenGL 1.1, texture objects and bind-to-*

The specification for OpenGL 1.1 was released in 1997 but the interesting part of it came from the GL_EXT_texture_object published a year and a half earlier. It acknowledges that respecifying the texture all the time is a problem, and that the work around used by developers, putting the glTexImage2D in a display-list, is not convenient. Here’s the abstract for the extension:

The only way to name a texture in GL 1.0 is by defining it as a single display list. Because display lists cannot be edited, these objects are static. Yet it is important to be able to change the images and parameters of a texture.

Interestingly they wanted to be able to change things like the size and format of the texture which in retrospect wasn’t a great idea. OpenGL’s glTexStorage and the modern API texture creation functions only allow specifying immutable texture: the content and sampling parameters can change but not the format and size. The problem with resizing textures is that the driver will have to reallocate the texture and garbage collect the previous allocation when it is no longer used, making the driver more complex.

The extension then defines how to create and use texture objects. At this point it could have been done in many different ways that would preserve backwards compatibility:

  1. Reuse the glTexImage2D call and make it act on a currently “bound” texture name.
  2. Reuse the glTexImage2D call, allowing texture names in place of GL_TEXTURE_2D for the first argument.
  3. Add a glTextureStorage2D-like call as in GL_ARB_direct_state_access, that takes only texture names as first argument.

All of these methods of defining texture objects have drawbacks. Method 2 is mostly good except that to preserve compatibility, texture names can’t be equal to GL_TEXTURE_2D. It would have been an annoying restriction because bind-to-create allows developers to choose the name of their texture objects (see below). Method 3 is okay but would have required more refactoring for developers to take advantage of. On the contrary methods 1 and 2 just require adding a glBindTexture call before the texture specification calls. Method 1 isn’t super pretty but doesn’t have drawbacks (at least until much later), and was the one chosen.

Apart from display lists, textures were the first object introduced in the OpenGL specification. This set a precedent and other object introduced followed the same pattern of glGen / glBind / glDelete even if they didn’t have backwards compatibility problems, like for GL_ARB_vertex_buffer_object. This pattern of having to bind objects to modify them is the dreaded bind-to-edit and it is only in 2014 that GL_ARB_direct_state_access became available to use Method 3 (and it isn’t even comprehensive) [2].

This extension made another important choice: using GLuint as the type for OpenGL object names. Arguably it would have been better in the long run if it had been an opaque pointer like the C stdlib FILE type. I believe GLuint was chosen because the authors wanted glBindTexture able to create names for two key reasons:

  • Developers can hand-allocate the texture names in their program (#define GRASS_TEXTURE 1000) for small performance gains.
  • OpenGL being a client-server architecture, glGenTextures forces an expensive roundtrip, while bind-to-create is a form of promise pipelining and causes no roundtrips.

glActiveTexture in OpenGL 1.2.1 and beyond

For this post, OpenGL 1.2 is fairly uninteresting but it had one revision, OpenGL 1.2.1 that adds the GL_ARB_multitexture extension, the first ARB extension, at page 239 of the specification. It adds the ability to sample multiple textures in each fragment along with many knobs to control how these textures are combined. This made it possible to do more advanced effects in one pass, and was used heavily in games like Quake 3.

As usual in OpenGL, GL_ARB_multitexture added functionality on top of OpenGL 1.2, reusing entry-points while keeping compatibility: it adds an active texture unit selector that influence which texture unit is modified by various OpenGL calls. By making the regular texture unit the default selected one, it ensured that OpenGL 1.2 programs would keep working without modifications. The function to set the active texture unit selector is, you guessed it, glActiveTexture. This means that to change the texture used by texture unit N, an application has to do the following:

glActiveTexture(GL_ACTIVE_TEXTURE0_ARB + N);
glBindTexture(GL_TEXTURE_2D, myTextureName);

This way of setting textures was reused as the pipeline became more and more programmable, going from multitexture to texture combiners, ARB assembly and GLSL shaders, each time reusing entry-points while keeping backwards compatibility. Even in OpenGL 4 core profile contexts, glBindTexture is still used to both set the active texture and select the texture to be configured by following calls, causing great developer pain.

Fin

This single-minded focus on reuse and compatibility made modern OpenGL awkward to use, distant from how the hardware works, and exhibit surprising behavior on a regular basis. The interaction of glActiveTexture and glBindTexture is just one of many examples.

The Khronos group had many opportunities to design an API from scratch but it seemed to have traded performance and developer sanity in order to have (mostly) interchangeable APIs:

  • OpenGL 3 was supposed to start from a clean state with Long Peaks but in the end was just like another revision on the same concepts.
  • OpenGL 3.2 introduces the concept of core profile, but the ARB only deprecated the inefficient and redundant ways to do things like glBegin and glEnd. This was a great improvement but it would have been even better if it had added GL_ARB_direct_state_access and deprecated bind-to-edit.
  • OpenGL 4.2 included GL_ARB_direct_state_access so that developers don’t have to bind-to-edit. However this version is not supported widely enough to be a minimum requirements for most applications and it doesn’t make sense to have both a bind-to-edit and DSA code paths.
  • OpenGL ES 2 is not compatible with OpenGL ES 1 and exposes features close to the desktop OpenGL core profile. It could have fixed many issues but didn’t, probably to stay close to desktop OpenGL.
  • WebGL was a new API, but chose to stay very close to OpenGL ES 2 (so as to not have to write a full specification?). Of note is that WebGL removes bind-to-create, but only because it wants OpenGL names to be opaque Javascript objects.
[1]Display lists were like command buffers in modern APIs, except that they could store all the OpenGL commands with few exceptions. This quickly became impractical to implement in hardware and the drivers ended up serializing and reading back the OpenGL commands. Modern command buffers are just display lists with a whitelist of commands instead of a blacklist. NV_command_list is an extension adding modern command buffers to OpenGL. Everything old is new again.
[2]to be fair, GL_EXT_direct_state_access was introduced in 2008, but it wasn’t supported by some major GPU vendors.