With GL, it's possible to write one code base that targets a very wide range of platforms, which is done by dynamically querying the hardware's capabilities. However, it's hard to know what are all the different ways to access a feature and what needs to be checked before using a feature. This web page attempts to keep track of all these kinds of decisions to make it easy to write highly compatible code.
From a high level perspective, each GL feature has different requirements to be available based on:
GL_ARB_texture_non_power_of_two
, GL_KHR_debug
, etc.)Furthermore, there exists quirks and bugs in GL implementations that will be documented here as they become known.
This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to http://unlicense.org/
It's your responsibility to fact-check the information on this page if you're doing serious work. If you do find a mistake on this page, please help everybody by opening an issue or submitting a pull request on the GitHub repository for the website. You can also contact me by e-mail, which is available on my GitHub page.
There are lots of other websites that can help with OpenGL compatibility. Consider checking these out:
One great thing about OpenGL is its ability to dynamically query the features supported by the implementation in use,
which makes it possible to target a wide variety of different OpenGL implementations in one code base.
This is partly made possible through the concept of entry points,
which act as channels of communication between your application and the GL driver.
In practice, these entry points are simple C functions,
and they are the bread and butter of any OpenGL application.
For example, the commonly used function glDrawArrays
is an entry point that allows you to tell the GL to draw some geometry.
The available entry points depend on the capabilities of the GL implementation made available to your application.
Therefore, in order to write highly compatible applications,
we should be only using entry points that we are certain are available,
which can be done by following the guides in this very article.
When you are certain that an entry point is available,
you can query it through a xxxGetProcAddress
function,
which is slightly different between different platforms.
These query functions always take a string as input
(the name of the entry point) and return a pointer to a function that can be used as an entry point.
Note that a GL context must be made current on the thread that is querying these entry points.
The xxxGetProcAddress
functions typically return a type-less void pointer (in C: void*
)
which must be casted to a correct function pointer type in order to be called.
There exist typedef
s for these function pointer types in Khronos' OpenGL Registry
(namely, in the glext.h
, glcorearb.h
, glxext.h
, and wglext.h
headers availble for download on that page.)
Similar headers are available for OpenGL ES from the Khronos OpenGL ES Registry.
wglGetProcAddress
.
GetProcAddress
function
to query entry points from opengl32.dll
.NULL
. This is Windows-specific behaviour.glxGetProcAddress
.
NULL
.
glxGetProcAddress
being NULL
.eglGetProcAddress
.
NULL
when queried using this extension.For convenience for the rest of this article,
this information will be used to define a GetProc
function that makes it easy to query for entry points:
// somewhere previous #ifdef _WIN32 HMODULE hModuleOpenGL32 = LoadLibrary(TEXT("opengl32.dll")); #endif void* GetProc(const char* procname) { #ifdef _WIN32 void* proc = wglGetProcAddress(procname); if (proc == NULL) { proc = GetProcAddress(hModuleOpenGL32, procname); } return proc; #elif defined(EMSCRIPTEN) return eglGetProcAddress(procname); #else // TODO: More platforms! #endif }What follows is an example use of the above
GetProc
function:
#include <GL/glcorearb.h> // somewhere later... PFNGLGETSTRINGPROC glGetString = GetProc("glGetString"); const char* renderer = glGetString(GL_RENDERER); printf("GL_RENDERER: %s\n", renderer);If this is all too ugly for you, you can use one of the many OpenGL loading libraries.
Before doing almost anything else, you need to figure out what API you are using and what version it is.
This is done using the output of glGetString(GL_VERSION)
, which is interpreted as follows:
Thus, it is necessary to check if the string begins with "WebGL" or "OpenGL ES" or neither in order to determine what API is currently in use. The version of the API in use can then be determined by parsing the major_number.minor_number from the string.
The three OpenGL APIs will be described using the following enum
:
enum GLApi { GLApi_OpenGL, GLApi_OpenGLES, GLApi_WebGL };
With this code, we can determine the API and its version:
const char* version = glGetString(GL_VERSION); if (strncmp(version, "WebGL ", 6) == 0) { myApi = GLApi_WebGL; version += 6; } else if (strncmp(version, "OpenGL ES ", 10) == 0) { myApi = GLApi_OpenGLES; version += 10; } else { myApi = GLApi_OpenGL; } sscanf(version, "%d.%d", &myMajorVersion, &myMinorVersion);For convenience, this information is used to define a
VersionCheck
function that makes it easy to check for API versions:
int VersionCheck(GLApi api, int major, int minor) { return myApi == api && (myMajorVersion > major || (myMajorVersion == major && myMinorVersion >= minor)); }
There are two ways to get the list of extensions depending on the version of the GL:
glGetStringi(GL_EXTENSIONS, i)
and glGetString(GL_EXTENSIONS)
.
The first method allows you to iterate over individual extensions by their index,
while the second method returns a big string that stores the names of the supported extensions separated by spaces.
The first method is more convenient, but only available in more modern implementations.
The second method is slightly inconvenient because it requires parsing the extension string.
It should be noted that WebGL has its own specialized functions for checking a specific extension or reading the list of extensions,
which are getExtension
and getSupportedExtensions
, respectively.
If you are using WebGL through emscripten, calls of glGetString(GL_EXTENSIONS)
automatically get translated to those WebGL-specific calls.
glGetStringi
is available.glGetString
(or use the special WebGL versions if you so desire.)int ExtensionCheck(const char* extension) { if (VersionCheck(GLApi_OpenGL, 3, 0) || VersionCheck(GLApi_OpenGLES, 3, 0)) { PFNGLGETINTEGERVPROC glGetIntegerv = GetProc("glGetIntegerv"); PFNGLGETSTRINGIPROC glGetStringi = GetProc("glGetStringi"); GLint numExtensions; glGetIntegerv(GL_NUM_EXTENSIONS, &numExtensions); for (GLint i = 0; i < numExtensions; i++) { const char* currExtension = glGetStringi(GL_EXTENSIONS, i); if (strcmp(extension, currExtension) == 0) { return 1; } } } else if (VersionCheck(GLApi_OpenGL, 1, 0) || VersionCheck(GLApi_OpenGLES, 1, 0) || VersionCheck(GLApi_WebGL, 1, 0)) { PFNGLGETSTRINGPROC glGetString = GetProc("glGetString"); const char* extensions = glGetString(GL_EXTENSIONS); while (1) { const char* next = strchr(extensions, ' '); if ((next != NULL && strncmp(extension, extensions, next - extensions) == 0) || (next == NULL && strcmp(extension, extensions) == 0)) { return 1; } if (next == NULL) break; extensions = next + 1; } } return 0; }
GL_CLAMP_TO_EDGE
Did you know GL_CLAMP_TO_EDGE
didn't always exist?
Yeah, there was a point where pretty much everything was tiled, so people just used GL_REPEAT
I suppose.
It was originally exposed as GL_SGIS_texture_edge_clamp
and quickly became a standard feature of GL implementations.
In the SGIS
implementation, it was exposed as the constant GL_CLAMP_TO_EDGE_SGIS
.
Thankfully, this constant has the same value as the standard GL_CLAMP_TO_EDGE
,
so your code can just use GL_CLAMP_TO_EDGE
without the SGIS
prefix when this feature is supported.
int clampToEdgeSupported = 0; if (VersionCheck(GLApi_OpenGL, 1, 2) || VersionCheck(GLApi_OpenGLES, 1, 0) || VersionCheck(GLApi_WebGL, 1, 0) || (VersionCheck(GLApi_OpenGL, 1, 0) && ExtensionCheck("GL_SGIS_texture_edge_clamp"))) { clampToEdgeSupported = 1; }
GL_ARB_vertex_buffer_object
Vertex buffers are a pretty awesome feature. They allow you to allocate GPU memory and upload data to them up front, so you don't need to constantly re-submit your geometry every time you want to draw. Vertex buffers are so useful that they became widely available in GL pretty early on. However, reading back from buffers and mapping buffers are areas where OpenGL ES and WebGL fall behind.
PFNGLBINDBUFFERPROC glBindBuffer = NULL; PFNGLDELETEBUFFERSPROC glDeleteBuffers = NULL; PFNGLGENBUFFERSPROC glGenBuffers = NULL; PFNGLISBUFFERPROC glIsBuffer = NULL; PFNGLBUFFERDATAPROC glBufferData = NULL; PFNGLBUFFERSUBDATAPROC glBufferSubData = NULL; PFNGLGETBUFFERSUBDATAPROC glGetBufferSubData = NULL; PFNGLMAPBUFFERPROC glMapBuffer = NULL; PFNGLUNMAPBUFFERPROC glUnmapBuffer = NULL; PFNGLGETBUFFERPARAMETERIVPROC glGetBufferParameteriv = NULL; PFNGLGETBUFFERPOINTERVPROC glGetBufferPointerv = NULL; if (VersionCheck(GLApi_OpenGL, 1, 5) || VersionCheck(GLApi_OpenGLES, 1, 1) || VersionCheck(GLApi_WebGL, 1, 0)) { glBindBuffer = GetProc("glBindBuffer"); glDeleteBuffers = GetProc("glDeleteBuffers"); glGenBuffers = GetProc("glGenBuffers"); glIsBuffer = GetProc("glIsBuffer"); glBufferData = GetProc("glBufferData"); glBufferSubData = GetProc("glBufferSubData"); if (VersionCheck(GLApi_OpenGL, 1, 5)) { glGetBufferSubData = GetProc("glGetBufferSubData"); glMapBuffer = GetProc("glMapBuffer"); glUnmapBuffer = GetProc("glUnmapBuffer"); glGetBufferPointerv = GetProc("glGetBufferPointerv"); } else if (VersionCheck(GLApi_OpenGLES, 1, 1) && ExtensionCheck("GL_OES_mapbuffer")) { glMapBuffer = GetProc("glMapBufferOES"); glUnmapBuffer = GetProc("glUnmapBufferOES"); glGetBufferPointerv = GetProc("glGetBufferPointervOES"); } glGetBufferParameteriv = GetProc("glGetBufferParameteriv"); } else if (VersionCheck(GLApi_OpenGL, 1, 0) && ExtensionCheck("GL_ARB_vertex_buffer_object")) { glBindBuffer = GetProc("glBindBufferARB"); glDeleteBuffers = GetProc("glDeleteBuffersARB"); glGenBuffers = GetProc("glGenBuffersARB"); glIsBuffer = GetProc("glIsBufferARB"); glBufferData = GetProc("glBufferDataARB"); glBufferSubData = GetProc("glBufferSubDataARB"); glGetBufferSubData = GetProc("glGetBufferSubDataARB"); glMapBuffer = GetProc("glMapBufferARB"); glUnmapBuffer = GetProc("glUnmapBufferARB"); glGetBufferPointerv = GetProc("glGetBufferPointervARB"); }
GL_ARB_map_buffer_range
GL_ARB_vertex_array_object
Storing the setup of your vertex data in a vertex array object is a commonly available extension on new-ish implementations. The interface for manipulating these vertex array objects is confusing because it uses so much global state, so I recommend taking the time to read the actual GL specs to really understand what state is being stored in a VAO and how to manipulate this state. (Protip: There are state tables at the end of GL specification documents.)
Sometimes using VAOs to store vertex array state is optional (and you can use a default global VAO instead), but some modern implementations require you to have a VAO bound if you want to make any draw calls. If you don't like this restriction, then just generate a single VAO and bind it at the start of your program!
Note that all the functions for actually manipulating VAO state come from other extensions. VAOs have become a bit of a chimera because of this.
PFNGLBINDVERTEXARRAYPROC glBindVertexArray = NULL; PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays = NULL; PFNGLGENVERTEXARRAYSPROC glGenVertexArrays = NULL; PFNGLISVERTEXARRAYPROC glIsVertexArray = NULL; if (VersionCheck(GLApi_OpenGL, 3, 0) || VersionCheck(GLApi_OpenGLES, 3, 0) || VersionCheck(GLApi_WebGL, 2, 0) || (VersionCheck(GLApi_OpenGL, 1, 0) && ExtensionCheck("GL_ARB_vertex_array_object"))) { // note: interestingly, GL_ARB_vertex_array_object entry points aren't defined with the ARB suffix. glBindVertexArray = GetProc("glBindVertexArray"); glDeleteVertexArrays = GetProc("glDeleteVertexArrays"); glGenVertexArrays = GetProc("glGenVertexArrays"); glIsVertexArray = GetProc("glIsVertexArray"); } else if (VersionCheck(GLApi_OpenGL, 1, 0) && ExtensionCheck("GL_APPLE_vertex_array_object")) { glBindVertexArray = GetProc("glBindVertexArrayAPPLE"); glDeleteVertexArrays = GetProc("glDeleteVertexArraysAPPLE"); glGenVertexArrays = GetProc("glGenVertexArraysAPPLE"); glIsVertexArray = GetProc("glIsVertexArrayAPPLE"); } else if ((VersionCheck(GLApi_OpenGLES, 1, 0) && ExtensionCheck("GL_OES_vertex_array_object")) || (VersionCheck(GLApi_WebGL, 1, 0) && ExtensionCheck("OES_vertex_array_object"))) { glBindVertexArray = GetProc("glBindVertexArrayOES"); glDeleteVertexArrays = GetProc("glDeleteVertexArraysOES"); glGenVertexArrays = GetProc("glGenVertexArraysOES"); glIsVertexArray = GetProc("glIsVertexArrayOES"); }
GL_ARB_instanced_arrays
This extension made it possible to use share a vertex attribute between multiple consecutive instanced draws.
PFNGLVERTEXATTRIBDIVISORPROC glVertexAttribDivisor = NULL; if (VersionCheck(GLApi_OpenGL, 3, 3) || VersionCheck(GLApi_OpenGLES, 3, 0) || VersionCheck(GLApi_WebGL, 2, 0)) { glVertexAttribDivisor = GetProc("glVertexAttribDivisor"); } else if (VersionCheck(GLApi_OpenGL, 1, 1) && ExtensionCheck("GL_ARB_instanced_arrays")) { glVertexAttribDivisor = GetProc("glVertexAttribDivisorARB"); } else if (VersionCheck(GLApi_OpenGLES, 2, 0)) { if (ExtensionCheck("GL_ANGLE_instanced_arrays")) { glVertexAttribDivisor = GetProc("glVertexAttribDivisorANGLE"); } else if (ExtensionCheck("GL_NV_instanced_arrays")) { glVertexAttribDivisor = GetProc("glVertexAttribDivisorNV"); } else if (ExtensionCheck("GL_EXT_instanced_arrays")) { glVertexAttribDivisor = GetProc("glVertexAttribDivisorEXT"); } } else if (VersionCheck(GLApi_WebGL, 1, 0) && ExtensionCheck("ANGLE_instanced_arrays")) { glVertexAttribDivisor = GetProc("glVertexAttribDivisorANGLE"); }
GL_ARB_vertex_attrib_binding
At some point, somebody thought it was a good idea to separate the format of attributes and the buffers they are bound to.
This is arguably a good idea, but it makes a lot of state in VAOs weirdly redundant.
(Namely, the "pointer" set from glVertexAttribPointer
now becomes an arbitrary userdata pointer which you can set to whatever you want,
since it now only has use in compatibility mode.)
Take the time to really read and understand the specs if you want to use this feature.
PFNGLBINDVERTEXBUFFERPROC glBindVertexBuffer = NULL; PFNGLVERTEXATTRIBFORMATPROC glVertexAttribFormat = NULL; PFNGLVERTEXATTRIBBINDINGPROC glVertexAttribBinding = NULL; PFNGLVERTEXBINDINGDIVISORPROC glVertexBindingDivisor = NULL; if (VersionCheck(GLApi_OpenGL, 4, 3) || (VersionCheck(GLApi_OpenGL, 4, 2) && ExtensionCheck("GL_ARB_vertex_attrib_binding")) || VersionCheck(GLApi_OpenGLES, 3, 1)) { glBindVertexBuffer = GetProc("glBindVertexBuffer"); glVertexAttribFormat = GetProc("glVertexAttribFormat"); glVertexAttribBinding = GetProc("glVertexAttribBinding"); glVertexBindingDivisor = GetProc("glVertexBindingDivisor"); }
glBindVertexBuffers
This function allows you to make multiple glBindVertexBuffer
calls at the time time.
PFNGLBINDVERTEXBUFFERSPROC glBindVertexBuffers = NULL; if (VersionCheck(GLApi_OpenGL, 4, 4)) { glBindVertexBuffers = GetProc("glBindVertexBuffers"); }
GL_KHR_debug
This extension adds lots of debug functionality to GL, which is usually only available in debug contexts.
Subsets of it are available if you support GL_[ARB|AMD]_debug_output
.
It should be noted that the AMD
version of this extension uses a different signature for the debug callback.
(In general, you should have a separate code path if you're using the AMD
extension for debugging.)
Also, GL_KHR_debug
exposes significantly more features than the plain GL_[ARB|AMD]_debug_output
extensions.
PFNGLDEBUGMESSAGEENABLEAMDPROC glDebugMessageEnableAMD = NULL; PFNGLDEBUGMESSAGEINSERTAMDPROC glDebugMessageInsertAMD = NULL; PFNGLDEBUGMESSAGECALLBACKAMDPROC glDebugMessageCallbackAMD = NULL; PFNGLGETDEBUGMESSAGELOGAMDPROC glGetDebugMessageLogAMD = NULL; PFNGLDEBUGMESSAGECONTROLPROC glDebugMessageControl = NULL; PFNGLDEBUGMESSAGEINSERTPROC glDebugMessageInsert = NULL; PFNGLDEBUGMESSAGECALLBACKPROC glDebugMessageCallback = NULL; PFNGLGETDEBUGMESSAGELOGPROC glGetDebugMessageLog = NULL; PFNGLGETPOINTERVPROC glGetPointerv = NULL; PFNGLPUSHDEBUGGROUPPROC glPushDebugGroup = NULL; PFNGLPOPDEBUGGROUPPROC glPopDebugGroup = NULL; PFNGLOBJECTLABELPROC glObjectLabel = NULL; PFNGLGETOBJECTLABELPROC glGetObjectLabel = NULL; PFNGLOBJECTPTRLABELPROC glObjectPtrLabel = NULL; PFNGLGETOBJECTPTRLABEL glGetObjectPtrLabel = NULL; if (VersionCheck(GLApi_OpenGL, 4, 3) || (VersionCheck(GLApi_OpenGL, 1, 1) && ExtensionCheck("GL_KHR_debug"))) { glDebugMessageControl = GetProc("glDebugMessageControl"); glDebugMessageInsert = GetProc("glDebugMessageInsert"); glDebugMessageCallback = GetProc("glDebugMessageCallback"); glGetDebugMessageLog = GetProc("glGetDebugMessageLog"); glGetPointerv = GetProc("glGetPointerv"); glPushDebugGroup = GetProc("glPushDebugGroup"); glPopDebugGroup = GetProc("glPopDebugGroup"); glObjectLabel = GetProc("glObjectLabel"); glGetObjectLabel = GetProc("glGetObjectLabel"); glObjectPtrLabel = GetProc("glObjectPtrLabel"); glGetObjectPtrLabel = GetProc("glGetObjectPtrLabel"); } else if (VersionCheck(GLApi_OpenGL, 1, 1) && ExtensionCheck("GL_ARB_debug_output")) { glDebugMessageControl = GetProc("glDebugMessageControlARB"); glDebugMessageInsert = GetProc("glDebugMessageInsertARB"); glDebugMessageCallback = GetProc("glDebugMessageCallbackARB"); glGetDebugMessageLog = GetProc("glGetDebugMessageLogARB"); glGetPointerv = GetProc("glGetPointerv"); } else if (VersionCheck(GLApi_OpenGL, 1, 1) && ExtensionCheck("GL_AMD_debug_output")) { glDebugMessageEnableAMD = GetProc("glDebugMessageEnableAMD"); glDebugMessageInsertAMD = GetProc("glDebugMessageInsertAMD"); glDebugMessageCallbackAMD = GetProc("glDebugMessageCallbackAMD"); glGetDebugMessageLogAMD = GetProc("glDebugMessageLogAMD"); } else if (VersionCheck(GLApi_OpenGLES, 1, 0) && ExtensionCheck("GL_KHR_debug")) { glDebugMessageControl = GetProc("glDebugMessageControlKHR"); glDebugMessageInsert = GetProc("glDebugMessageInsertKHR"); glDebugMessageCallback = GetProc("glDebugMessageCallbackKHR"); glGetDebugMessageLog = GetProc("glGetDebugMessageLogKHR"); glGetPointerv = GetProc("glGetPointervKHR"); glPushDebugGroup = GetProc("glPushDebugGroupKHR"); glPopDebugGroup = GetProc("glPopDebugGroupKHR"); glObjectLabel = GetProc("glObjectLabelKHR"); glGetObjectLabel = GetProc("glGetObjectLabelKHR"); glObjectPtrLabel = GetProc("glObjectPtrLabelKHR"); glGetObjectPtrLabel = GetProc("glGetObjectPtrLabelKHR"); }
GL_ARB_texture_non_power_of_two
Not all OpenGL implementations can use NPOT textures freely: Some can't use them at all, some can use them but not repeat or mipmap them, while more modern implementations allow all operations on NPOT textures.
This is pretty much adapted right out of Aras' awesome blog post on this topic, which you should read if you want more information about NPOT texture support.
// See Aras' blog post to understand what these classifications mean. // Can do convenient stuff, like "if (npotSupport >= NPOTSupport_Limited)" enum NPOTSupport { NPOTSupport_None, NPOTSupport_Limited, NPOTSupport_Full }; NPOTSupport npotSupport = NPOTSupport_None; if (VersionCheck(GLApi_OpenGL, 3, 0) || VersionCheck(GLApi_OpenGLES, 3, 0) || VersionCheck(GLApi_WebGL, 2, 0)) { npotSupport = NPOTSupport_Full; } else if (VersionCheck(GLApi_OpenGL, 2, 0) || (VersionCheck(GLApi_OpenGL, 1, 0) && ExtensionCheck("GL_ARB_texture_non_power_of_two"))) { // TODO: Attempt to find if a DX10+ level GPU is running to use full NPOT support. // See Aras' blog post for some heuristics. npotSupport = NPOTSupport_Limited; } else if (VersionCheck(GLApi_OpenGLES, 1, 0)) { if (ExtensionCheck("GL_ARB_texture_non_power_of_two") || ExtensionCheck("GL_OES_texture_npot")) { npotSupport = NPOTSupport_Full; } else if (ExtensionCheck("GL_APPLE_texture_2D_limited_npot") || ExtensionCheck("GL_IMG_texture_npot")) { npotSupport = NPOTSupport_Limited; } } else if (VersionCheck(GLApi_WebGL, 1, 0)) { npotSupport = NPOTSupport_Limited; }