In a Nutshell: Design of OpenGL

6 minute read

This post will talk about the design of OpenGL as an API. It will go over OpenGL’s syntax including function naming conventions and explain the reasoning behind them. Additionally, it talk about OpenGL’s state machine, the object constructs used by the API and a new way of interacting with OpenGL state called Direct State Access (DSA) along with the benefits it brings to the API. Any code within this post should be considered pseudo-code and is mainly used for examples about concepts mentioned in text.

The Design of OpenGL

OpenGL is implemented as a client-server system. The application is considered the client, while the implementation of OpenGL provided by the graphics hardware manufacturer is considered the server. Most modern implementations use a hardware graphics accelerator to implement most of the OpenGL specification. This accelerator can either be embedded in the CPU or a separate hardware component plugged into the computer’s motherboard (discrete GPU).

OpenGL Syntax

OpenGL being a C API cannot utilize classes, namespaces and function overloading to differentiate between function names. For this reason, all functions in the OpenGL library begin with the prefix gl and are immediately followed by one or more capitalized words that complete the function name (for example, glCreateVertexArrays()). To circumvent the lack of function overloading specifically, OpenGL functions that expect arguments use a naming convention that indicates the number and the type of arguments, for example glUniform1i expects one integer as an argument and glUniform4fv expects an array (vector) of four floating-point numbers.

The following code snippet shows how OpenGL function names describe the number and type of arguments expected to be provided by the user.

// For example: 
// glUniform1i expects the shader uniform location 
// and 1 integer to be provided...
GLint value = 10;
glUniform1i(uniformLocation1, value);

// ...while glUniform4fv expects the shader 
// uniform location and an array(vector) of 4 floats.
std::array<GLfloat, 4> values{ 1.0f, 2.0f, 3.0f, 4.0f };
glUniform4fv(uniformLocation2, values.size(), values.data());

OpenGL’s constants are defined using capitalized letters starting with the prefix GL_ and use underscores as word separators. Additionally, because OpenGL supports multiple operating systems, it defines various data types to avoid type size mismatches (for example GLfloat is the floating-point data type).

The following code snippet demonstrates OpenGL function calls that use various OpenGL constants as well as OpenGL’s data types.


GLint texId = 0;
glGenTextures(1, &texId);

glBindTexture(GL_TEXTURE_2D, texId);

glTexParameteri(GL_TEXTURE_2D,
                GL_TEXTURE_MIN_FILTER,
                GL_NEAREST);

The OpenGL State Machine

OpenGL is designed as a state machine. The process of outputting geometry to the framebuffer requires maintaining many state settings. These state settings do not affect one another, so setting one piece of state has no side effects to other state settings. The collection of all the settings defined, determine the behavior of the graphics pipeline and the way that primitives are transformed to pixels to be displayed on the display device. OpenGL maintains its state in an opaque data structure known as the graphics context. Graphics contexts are created or deleted and assigned to an OpenGL framebuffer by window- system-specific functions. OpenGL’s state is divided into two categories, the server state and the client state. Most of these values have two available states, enabled or disabled, and the functions that perform the state change are glEnable and glDisable for the server-side state and glEnableClientState and glDisableClientState for the client-side state.

The following code snippet show an example of disabling and enabling server-side state.


// Disable depth testing
glDisable(GL_DEPTH_TEST)

// Disable back face culling
glDisable(GL_CULL_FACE)

// Perform a draw operation using the current state.
glDrawArrays(...)

// Restore previous state
glEnable(GL_DEPTH_TEST)
glEnable(GL_CULL_FACE)

OpenGL Objects

All of OpenGL’s constructs are backed by an OpenGL object. OpenGL objects are represented by an unsigned integer (GLuint). They contain state and are bound to the context so that their state can be mapped to the context’s state. If the object is bound, any changes to the context state will be stored in this object and any functions that act on the context state will use the state stored in the bound object.

OpenGL objects are essentially a way to aggregate state and apply it to the context with one function call. They are created using the glGen* group of functions (for example glGenTextures()) and are deleted using the glDelete* group of functions.

For OpenGL objects to be modified they must be bound to the OpenGL context. This is needed because OpenGL objects are defined as collections of state, and when bound to the context, the state they encapsulate becomes the context’s current state. This means that any modification to the state after an object is bound will affect only its own state. OpenGL objects are bound to the context using the glBind* group of functions (for example glBindTexture()).

OpenGL objects can be divided into two categories, regular objects and container objects. The following table lists the objects in both categories:

Regular Objects Container Objects
Buffer Framebuffer
Query Program Pipeline
Renderbuffer Vertex Array
Sampler Transform Feedback
Texture  

OpenGL uses a special GLuint value to represent the absence of an object or indicate a default state version of an object. This value is 0 and is known as Object Zero. For most object types, object zero represents a kind of null pointer, meaning that it is not an actual object. If object zero is bound for these object types, attempting to use them for rendering will fail. For some objects, binding object zero represents a default state. For example, invoking glBindFramebuffer with object name zero means that the default OpenGL framebuffer should be used.

Newer versions of OpenGL introduced a new way of modifying object state. Since OpenGL 4.5, object state can be modified without the need to bind the objects to the context. This new way of modifying object state is called Direct State Access (DSA). DSA makes OpenGL more object oriented following the paradigm of other APIs like Vulkan or Microsoft’s Direct3D. This allows for functions that modify state to be clearly identified. The naming of DSA functions is more consistent than the non-DSA function naming. It follows the standard Verb-Object-Command syntax like non-DSA functions, but they always require the object name to be specified, a fact that increases the consistency of the API. DSA functions also differentiate from the non-DSA functions by using different object names in their signature. This is done to remove any confusion between DSA and non-DSA function signatures due to the lack of function overloading in the C programming language.

OpenGL Object Type Context Object Name DSA Object Name
Texture Object “Tex” “Texture”
Framebuffer Object “Framebuffer” “NamedFramebuffer”
Buffer Object “Buffer” “NamedBuffer”
Transform Feedback Object “TransformFeedback” “TransformFeedback”
Vertex Array Object N/A “VertexArray”
Sampler Object N/A “Sampler”
Program Object N/A “Program”
Query Object N/A “Query”

One of the biggest advantages of DSA is that it makes it easier to compose libraries together that might modify or change OpenGL state. It also makes it easier for OpenGL to be wrapped in an object-oriented or functional way. Additionally, it is easier to create bindings of the API for various non-C languages and to create cross API implementation since other APIs are already using an object-oriented approach.

The following code snippets demonstrate the difference in verbosity between DSA and non-DSA OpenGL code.

Without DSA

// Specify active texture binding
glActiveTexture(GL_TEXTURE0);

// Bind the texture objet with name "textureId" to 
// the TEXTURE_2D texture target
glBindTexture(GL_TEXTURE_2D, textureId);

// Configure bound texture state
glTexParameteri(GL_TEXTURE_2D,
                GL_TEXTURE_MIN_FILTER,
                GL_LINEAR);
                
// Bind object zero to restore default state
glBindTexture(GL_TEXTURE_2D, 0);

With DSA

// Directly access the texture state
// by providing the object name to the function call
glTextureParameteri(textureId,
                    GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR);

References

Updated:

Comments