What is OpenGL

Before we can begin looking into writing an OpenGL application, we must first know what it is that we are writing. What exactly is OpenGL?

OpenGL as an API

OpenGL is usually thought of as an Application Programming Interface (API). The OpenGL API has been exposed to a number of languages. But the one that they all ultimately use at their lowest level is the C API.

The API, in C, is defined by a number of typedefs, #defined enumerator values, and functions. The typedefs define basic GL types like GLint, GLfloat and so forth. These are defined to have a specific bit depth.

Complex aggregates like structs are never directly exposed in OpenGL. Any such constructs are hidden behind the API. This makes it easier to expose the OpenGL API to non-C languages without having a complex conversion layer.

In C++, if you wanted an object that contained an integer, a float, and a string, you would create it and access it like this:

struct Object
{
    int count;
    float opacity;
    char *name;
};

//Create the storage for the object.
Object newObject;

//Put data into the object.
newObject.count = 5;
newObject.opacity = 0.4f;
newObject.name = "Some String";

In OpenGL, you would use an API that looks more like this:

//Create the storage for the object
GLuint objectName;
glGenObject(1, &objectName);

//Put data into the object.
glBindObject(GL_MODIFY, objectName);
glObjectParameteri(GL_MODIFY, GL_OBJECT_COUNT, 5);
glObjectParameterf(GL_MODIFY, GL_OBJECT_OPACITY, 0.4f);
glObjectParameters(GL_MODIFY, GL_OBJECT_NAME, "Some String");

None of these are actual OpenGL commands, of course. This is simply an example of what the interface to such an object would look like.

OpenGL owns the storage for all OpenGL objects. Because of this, the user can only access an object by reference. Almost all OpenGL objects are referred to by an unsigned integer (the GLuint). Objects are created by a function of the form glGen*, where * is the type of the object. The first parameter is the number of objects to create, and the second is a GLuint* array that receives the newly created object names.

To modify most objects, they must first be bound to the context. Many objects can be bound to different locations in the context; this allows the same object to be used in different ways. These different locations are called targets; all objects have a list of valid targets, and some have only one. In the above example, the fictitious target GL_MODIFY is the location where objectName is bound.

The enumerators GL_OBJECT_* all name fields in the object that can be set. The glObjectParameter family of functions set parameters within the object bound to the given target. Note that since OpenGL is a C API, it has to name each of the differently typed variations differently. So there is glObjectParameteri for integer parameters, glObjectParameterf for floating-point parameters, and so forth.

Note that all OpenGL objects are not as simple as this example, and the functions that change object state do not all follow these naming conventions. Also, exactly what it means to bind an object to the context is explained below.

The Structure of OpenGL

The OpenGL API is defined as a state machine. Almost all of the OpenGL functions set or retrieve some state in OpenGL. The only functions that do not change state are functions that use the currently set state to cause rendering to happen.

You can think of the state machine as a very large struct with a great many different fields. This struct is called the OpenGL context, and each field in the context represents some information necessary for rendering.

Objects in OpenGL are thus defined as a list of fields in this struct that can be saved and restored. Binding an object to a target within the context causes the data in this object to replace some of the context's state. Thus after the binding, future function calls that read from or modify this context state will read or modify the state within the object.

Objects are usually represented as GLuint integers; these are handles to the actual OpenGL objects. The integer value 0 is special; it acts as the object equivalent of a NULL pointer. Binding object 0 means to unbind the currently bound object. This means that the original context state, the state that was in place before the binding took place, now becomes the context state.

Let us say that this represents some part of an OpenGL context's state:

Example 1. OpenGL Object State

struct Values
{
    int iValue1;
    int iValue2;
};

struct OpenGL_Context
{
    ...
    Values *pMainValues;
    Values *pOtherValues;
    ...
};

OpenGL_Context context;

To create a Values object, you would call something like glGenValues. You could bind the Values object to one of two targets: GL_MAIN_VALUES which represents the pointer context.pMainValues, and GL_OTHER_VALUES which represents the pointer context.pOtherValues. You would bind the object with a call to glBindValues, passing one of the two targets and the object. This would set that target's pointer to the object that you created.

There would be a function to set values in a bound object. Say, glValueParam. It would take the target of the object, which represents the pointer in the context. It would also take an enum representing which value in the object to change. The value GL_VALUE_ONE would represent iValue1, and GL_VALUE_TWO would represent iValue2.

The OpenGL Specification

To be technical about it, OpenGL is not an API; it is a specification. A document. The C API is merely one way to implement the spec. The specification defines the initial OpenGL state, what each function does to change or retrieve that state, and what is supposed to happen when you call a rendering function.

The specification is written by the OpenGL Architectural Review Board (ARB), a group of representatives from companies like Apple, NVIDIA, and AMD (the ATI part), among others. The ARB is part of the Khronos Group.

The specification is a very complicated and technical document. However, parts of it are quite readable, though you will usually need at least some understanding of what should be going on to understand it. If you try to read it, the most important thing to understand about it is this: it describes results, not implementation. Just because the spec says that X will happen does not mean that it actually does. What it means is that the user should not be able to tell the difference. If a piece of hardware can provide the same behavior in a different way, then the specification allows this, so long as the user can never tell the difference.

OpenGL Implementations. While the OpenGL ARB does control the specification, it does not control OpenGL's code. OpenGL is not something you download from a centralized location. For any particular piece of hardware, it is up to the developers of that hardware to write an OpenGL Implementation for that hardware. Implementations, as the name suggests, implement the OpenGL specification, exposing the OpenGL API as defined in the spec.

Who controls the OpenGL implementation is different for different operating systems. On Windows, OpenGL implementations are controlled virtually entirely by the hardware makers themselves. On Mac OSX, OpenGL implementations are controlled by Apple; they decide what version of OpenGL is exposed and what additional functionality can be provided to the user. Apple writes much of the OpenGL implementation on Mac OSX, which the hardware developers writing to an Apple-created internal driver API. On Linux, things are... complicated.

The long and short of this is that if you are writing a program and it seems to be exhibiting off-spec behavior, that is the fault of the maker of your OpenGL implementation (assuming it is not a bug in your code). On Windows, the various graphics hardware makers put their OpenGL implementations in their regular drivers. So if you suspect a bug in their implementation, the first thing you should do is make sure your graphics drivers are up-to-date; the bug may have been corrected since the last time you updated your drivers.

OpenGL Versions. There are many versions of the OpenGL Specification. OpenGL versions are not like most Direct3D versions, which typically change most of the API. Code that works on one version of OpenGL will almost always work on later versions of OpenGL.

The only exception to this deals with OpenGL 3.0 and above, relative to previous versions. v3.0 deprecated a number of older functions, and v3.1 removed most of those functions from the API[1]. This also divided the specification into 2 variations (called profiles): core and compatibility. The compatibility profile retains all of the functions removed in 3.1, while the core profile does not. Theoretically, OpenGL implementations could implement just the core profile; this would leave software that relies on the compatibility profile non-functional on that implementation.

As a practical matter, none of this matters at all. No OpenGL driver developer is going to ship drivers that only implement the core profile. So in effect, this means nothing at all; all OpenGL versions are all effectively backwards compatible.



[1] Deprecation only means marking those functions as to be removed in later functions. They are still available for use in 3.0.