OpenGL modern context and extensions

Between all the difficult challenges in graphics programming, one of the most important requirement is to write scalable code capable of running in a wide spectrum of hardwares. When we talk about context in OpenGL we refer to the particular state of the API with respect to the specific hardware that runs it. I like how the OpenGL Wiki introduces this concept, using an oop comparison in which a context can be considered as an object instantiated from the “OpenGL class” (see https://www.khronos.org/opengl/wiki/OpenGL_Context). The most straightforward way to use OpenGL in your codebase is to delegate the context creation to an external library such as GLEW. But maybe if you feel ambitious, brave and crazy you might think to handle this cumbersome process all by yourself! On a more serious note, a common mindset between programmers is that building a feature from scratch, without relying on external tools even when they are “proven” to be effective, is equivalent to reinventing the wheel. The usual counter-argument to this statement is that the wheel has not been invented yet, at least in the filed of videogame programming, or that if the wheel actually exists then it is more similar to a squared one in reality. Whichever your position is in that regard, you might agree that having more control over your codebase, and limiting the influx of external dependencies at the same time is a desirable condition to achieve. Considering that the OpenGL context is a create-and-forget type of process, you are not paying this increased control with countless hours of development and maintenance. In this post i want to discuss my experience in creating a modern OpenGL context from scratch, describing also the process behind extension retrieval since it is closely related to the context itself. Hopefully this read is going to be helpful to some brave soul out there!

 

How to Create a Modern Context

So, here’s the deal with OpenGL contexts. Citing the Wiki: “Because OpenGL doesn’t exist until you create an OpenGL Context, OpenGL context creation is not governed by the OpenGL Specification. It is instead governed by platform-specific APIs.”. One consequence of this statement is that you need to code at least a very basic platform layer beforehand. In particular, the bare minimum feature that you need to implement is the creation of a window, so nothing too difficult to begin with. In my case i chose Windows and the Win32 API, and therefore i will be able to describe the context creation only relatively to this platform. As a first step i included <gl/GL.h> between the header files, getting access to all the needed OpenGL types, macros and functions. Then, my engine creates the application window specifying the CS_OWNDC macro along with the other desired styles. Remember that this is a mandatory step! Each window has an associated Device Context (DC) that in turn can store a Pixel Format, a structure describing the desired properties of the default framebuffer. By calling the Win32 function ChoosePixelFormat, Windows is going to select the closest match between the specified pixel format and a list of supported formats. Finally, the context can be created using SetPixelFormat and activated with wglMakeCurrent. Now, the pixel format can be filled in the old-fashioned way (using the values suggested in the OpenGL wiki), but this process is not capable of creating a “modern” OpenGL context. For modern context we intend an OpenGL configuration that allows to import relatively recent features that are considered to be very useful in modern graphics programming, such as sRGB textures and framebuffers, Multisampling, and many other. In order to create this modern context we need to call a function that is not natively present in core OpenGL, and this is where the concept of extensions starts to be relevant. Extensions are query-based functions that expand the core functionalities of OpenGL, and every program that wants to use them needs to check their availability in the specific graphic card, and eventually to import them. The Windows specific extensions are called WGL extensions, and in order to create a modern contex we need to call one of them: wglChoosePixelFormatARB. So, we can fill the pixel format in the old way in order to create a legacy context, or we can retrieve the WGL variant and fill the pixel format using a list of desired attributes instead, specified as OpenGL macros. If we decide to create a modern context (as we should), however, there is a last inconvenient to overcome. In order to retrieve wglChoosePixelFormat we need to have an active OpenGL context, hence we are forced to start with a dummy legacy context. Since Windows doesn’t allow to change the pixel format after it has been set in a first place, we need to create the modern context from scratch and to delete the dummy one. The following code snippet implements the logics we just described:

void setOpenGLDesiredPixelFormat(HDC windowDC)
{
	int suggestedPixelFormatIndex = 0;
	GLuint extendedPick = 0;

	if (wglChoosePixelFormatARB)
	{
		int intAttribList[] =
		{
			WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
			WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB,
			WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
			WGL_DOUBLE_BUFFER_ARB, GL_TRUE,
			WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
			WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB, GL_TRUE,
			0,
		};

		if (!GLOBAL_OpenGL_State.supportsSRGBframebuffers)
			intAttribList[10] = 0;

		wglChoosePixelFormatARB(windowDC, intAttribList, 0, 1, &suggestedPixelFormatIndex, &extendedPick);
	}

	if (!extendedPick)
	{
		PIXELFORMATDESCRIPTOR desiredPixelFormat = {};
		desiredPixelFormat.nSize = sizeof(desiredPixelFormat);
		desiredPixelFormat.nVersion = 1;
		desiredPixelFormat.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
		desiredPixelFormat.cColorBits = 32;
		desiredPixelFormat.cAlphaBits = 8;
		desiredPixelFormat.cDepthBits = 24;
		desiredPixelFormat.iLayerType = PFD_MAIN_PLANE;
		desiredPixelFormat.iPixelType = PFD_TYPE_RGBA;

		// We suggest a desired pixel format, and windows searches for a fitting one
		suggestedPixelFormatIndex = ChoosePixelFormat(windowDC, &desiredPixelFormat);
	}

	PIXELFORMATDESCRIPTOR suggestedPixelFormat;
	DescribePixelFormat(windowDC, suggestedPixelFormatIndex, sizeof(suggestedPixelFormat), &suggestedPixelFormat);
	SetPixelFormat(windowDC, suggestedPixelFormatIndex, &suggestedPixelFormat);
}

void loadWGLExtensions()
{
	// Create a dummy opengl context (window, DC and pixel format) in order to query the wgl extensions,
	// it is going to be deleted at the end of the function
	WNDCLASSA windowClass = {};
	windowClass.lpfnWndProc = DefWindowProcA;
	windowClass.hInstance = GetModuleHandle(0);
	windowClass.lpszClassName = "Venom Engine wgl loader";

	if (RegisterClassA(&windowClass))
	{
		HWND window = CreateWindowExA(0, windowClass.lpszClassName, "Venom Engine Window", 0, CW_USEDEFAULT,
			                      CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, windowClass.hInstance, 0);

		HDC windowDC = GetDC(window);
		setOpenGLDesiredPixelFormat(windowDC);
		HGLRC openGLRC = wglCreateContext(windowDC);

		if (wglMakeCurrent(windowDC, openGLRC))
		{
			// Since we are here, we might as well retrieve other useful WGL extensions.
			// The definition of the following macro is going to be discussed later
			Win32wglGetProcAddress(wglChoosePixelFormatARB);
			Win32wglGetProcAddress(wglCreateContextAttribsARB);
			Win32wglGetProcAddress(wglSwapIntervalEXT);
			Win32wglGetProcAddress(wglGetExtensionsStringEXT);

			if (wglGetExtensionsStringEXT)
			{
				// Parse extensions string
				char* extensions = (char*)wglGetExtensionsStringEXT();
				char* at = extensions;
				while (*at)
				{
					while (IsWhiteSpace(*at))
						++at;

					char* end = at;
					while (*end && !IsWhiteSpace(*end))
						++end;

					uintptr count = end - at;
					if (AreStringsEqual(count, at, "WGL_EXT_framebuffer_sRGB"))
						GLOBAL_OpenGL_State.supportsSRGBframebuffers = true;
					else if (AreStringsEqual(count, at, "WGL_ARB_framebuffer_sRGB"))
						GLOBAL_OpenGL_State.supportsSRGBframebuffers = true;

					at = end;
				}
			}

			wglMakeCurrent(0, 0);
		}

		wglDeleteContext(openGLRC);
		ReleaseDC(window, windowDC);
		DestroyWindow(window);
	}
}

Note how we save into a global state some information that derives from parsing the extensions string. It doesn’t have to be necessarily a global structure, you can choose to make it local and to pass it into the function, maybe also returning the same struct in order to share the information externally.

 

Extensions Retrieval

Since a modern game engine requires to have a great number of extensions, it can be useful to semi-automate their retrieval with the definition of some macro. Firstly, the needed extensions can be literally copy-pasted from https://www.khronos.org/registry/OpenGL/api/GL/glcorearb.h and from https://www.khronos.org/registry/OpenGL/api/GL/glext.h. A custom prefix can then be added before each function name in order to create their type (i use “vm_” in my engine), creating the function pointers in the following way:

// Macro that creates a function pointer for an extension.  
#define DefineOpenGLGlobalFunction(name) static vm_##name* name;

// Usage example: copy-paste the function from glcorearb.h, add any prefix and define the function pointer
// with the macro
typedef void WINAPIvm_glDebugMessageCallbackARB(GLDEBUGPROC* callback, const void *userParam);
DefineOpenGLGlobalFunction(glDebugMessageCallbackARB);

The extensions can be finally retrieved using wglGetProcAddress inside the following macro, remembering that the name is case sensitive. The initOpenGL function summarizes the entire process, from context creation to extension retrieval:

#define Win32wglGetProcAddress(name) name = (vm_##name*)wglGetProcAddress(#name);

// Example of desired attributes for the pixel format of a modern context
int win32OpenGLAttribs[] =
{
	WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
	WGL_CONTEXT_MINOR_VERSION_ARB, 3,
	WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB
#if DEBUG_MODE
	| WGL_CONTEXT_DEBUG_BIT_ARB
#endif
	,
	WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
	0,
};

void initOpenGL(HDC windowDC)
{
	loadWGLExtensions();
	setOpenGLDesiredPixelFormat(windowDC);

	bool8 modernContext = true;
	HGLRC openGLRC = 0;
	if (wglCreateContextAttribsARB)
		openGLRC = wglCreateContextAttribsARB(windowDC, 0, win32OpenGLAttribs);

	if (!openGLRC)
	{
		modernContext = false;
		openGLRC = wglCreateContext(windowDC);
	}

	if (wglMakeCurrent(windowDC, openGLRC))
	{
		// Extract all the desired extensions
		Win32wglGetProcAddress(glDebugMessageCallbackARB);
		...
	}
}

Be prepared to fill this section of the code with countless extensions!

 

Scalability

We are now able to initialize a modern OpenGL context but the engine might still run in a very old machine, and therefore it’s important to check every time some additional clue about the specific hardware. We could define a function that fills a struct with relevant informations about the state of the specific GPU. In particular, this function may check information about vendor, renderer, version, shading language version (eventually none, and the renderer falls back to the fixed pipeline) and then it can parse the extensions string. Here, the most important extensions usually are GL_EXT_texture_sRGB, GL_EXT_framebuffer_sRGB, GL_ARB_framebuffer_sRGB and GL_ARB_framebuffer_object, and for each one we can set a boolean in the return struct indicating if they are supported or not. In the next code snippet, wherever you see a string-related function that was not previously defined, feel free to use your favourite string library or your personal implementation. I have just inserted the syntax from the string library of my engine as a placeholder, but it should be straightforward to understand what each function does:

struct opengl_info
{
	char* vendor;
	char* renderer;
	char* version;
	char* shadingLanguageVersion;

	bool32 GL_EXT_texture_sRGB;
	bool32 GL_EXT_framebuffer_sRGB;
	bool32 GL_ARB_framebuffer_sRGB;
	bool32 GL_ARB_framebuffer_object;
	bool32 modernContext;
};

void parseExtensionsString(opengl_info* info)
{
	if (glGetStringi)
	{
		GLint extensionCount = 0;
		glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);

		for (GLint extensionIndex = 0; extensionIndex < extensionCount; ++extensionIndex)
		{
			char* extensionName = (char*)glGetStringi(GL_EXTENSIONS, extensionIndex);
			
			if (AreStringsEqual(extensionName, "GL_EXT_texture_sRGB"))
				info->GL_EXT_texture_sRGB = true;
			else if (AreStringsEqual(extensionName, "GL_EXT_framebuffer_sRGB"))
				info->GL_EXT_framebuffer_sRGB = true;
			else if (AreStringsEqual(extensionName, "GL_ARB_framebuffer_sRGB"))
				info->GL_ARB_framebuffer_sRGB = true;
			else if (AreStringsEqual(extensionName, "GL_ARB_framebuffer_object"))
				info->GL_ARB_framebuffer_object = true;
		}
	}
}

opengl_info getInfo(bool8 modernContext)
{
	opengl_info result = {};
	result.vendor = (char*)glGetString(GL_VENDOR);
	result.renderer = (char*)glGetString(GL_RENDERER);
	result.version = (char*)glGetString(GL_VERSION);
	result.modernContext = modernContext;

	if (result.modernContext)
		result.shadingLanguageVersion = (char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
	else
		result.shadingLanguageVersion = "(none)";

	parseExtensionsString(&result);
	checkSRGBTexturesSupport(&result);

	return result;
}

A usage example for this opengl_info struct is shown for the sRGB texture support in the next code snippet:

void checkSRGBTexturesSupport(opengl_info* info)
{
	char* majorAt = info->version;
	char* minorAt = 0;
	for (char* at = info->version; *at; ++at)
	{
		if (at[0] == '.')
		{
			minorAt = at + 1;
			break;
		}
	}

	int32 majorVersion = 1;
	int32 minorVersion = 0;
	if (minorAt)
	{
		majorVersion = StringToInt32(majorAt);
		minorVersion = StringToInt32(minorAt);
	}

	if ((majorVersion > 2) || ((majorVersion == 2) && (minorVersion >= 1)))
		info->GL_EXT_texture_sRGB = true;
}

This is just a first step in satisfying your scalability requirements, but the main concept will always be similar to what we have seen: query the machine about its hardware/software details, and write code that can handle the greatest number of usage cases. Sometimes it won’t be possible to satisfy every side, and that’s where you’ll have to make some choice. Of course it’s not wise to support very old hardwares and/or operating systems if you have to penalize some modern feature, but it’s rare to find easier decisions than this. Most of the time your approach is going to be dictated by complex strategies and situations, and you will have to work out the best solution capable of satisfying technology, management and marketing aspects.

That’s all i wanted to say about this topic for now. The next step after setting the modern context and a bunch of extensions is to actually start using OpenGL, so go on and draw that dreaded first triangle!