Win32 / OpenGL 3.x without GLEW/GLFW

2018-06-25 // 7 minutes

When I first learned OpenGL, every tutorial that I found skipped over how OpenGL is loaded through the operating system. Instead, their example code just used libraries like GLEW and GLFW. These libraries are fine and can be a helpful way to get started at the expense of some code flow control. But I wanted insight into how it works under the hood. Moreover, I was keen on minimizing library dependencies. (If you're a relatively new programmer, here's a good discussion on minimizing dependencies.)

This post will show you get started on a Win32/OpenGL application without any library dependencies beyond the code that ships with your OS. If you just want to see a working example, you can look at the code that I posted here.

NOTE: Even with Apple deprecating their support for OpenGL, I believe it's still worth learning. Not every team is going to immediately jump to Vulkan. You may also have to maintain a legacy project. Moreover, it seems that the prevailing attitude online has been that learning Vulkan without experience in OpenGL/D3D/etc is too much for a beginner to tackle. So learning OpenGL can show you how a graphics pipeline could work, which will give you a reference for learning Vulkan.

Creating a Win32 Window

One of the first things you will need to do is tell the operating system to create a window for you. The OpenGL context that you will create later gets attached to this window. Here's the documentation for the WNDCLASSEX data structure. And here's an example of how to define your window:

WNDCLASSEX windowClass;

windowClass.cbSize = sizeof(WNDCLASSEX);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.lpfnWndProc = win32_mainWindowCallback;
windowClass.cbClsExtra = 0;
windowClass.cbWndExtra = 0;
windowClass.hInstance = instance;
windowClass.hIcon = LoadIcon(instance, MAKEINTRESOURCE(IDI_APPLICATION));
windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
windowClass.lpszMenuName = NULL;
windowClass.lpszClassName = L"ExampleWindowClass";
windowClass.hIconSm = LoadIcon(windowClass.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));

Next up you will need to register your window class with the OS and tell it to actually create the window. Here's the documentation for registering and creating) a window. And here's the example code:

if (RegisterClassEx(&windowClass)) {
	window = CreateWindow(
		windowClass.lpszClassName,
		L"Example Window",
		WS_OVERLAPPEDWINDOW | WS_VISIBLE,
		CW_USEDEFAULT, CW_USEDEFAULT,
		1600, 800,
		NULL,
		NULL,
		instance,
		NULL
	);
}

Preparing to Load OpenGL Functions

In order to load and ultimately use functions from the OpenGL DLL, you will need to define a function pointer that calls into the OpenGL DLL. You could do this manually for each function, but as you might imagine that can get messy quickly. To get around that problem, we can write a macro that will help make our approach much more readable. The following code should go in your shareable header file.

// List out the OpenGL functions you want to load
#define OPENGL_LIST \
/*  ret, name, params */ \
GLE(void, GenVertexArrays, GLsizei n, GLuint *arrays); \
GLE(void, BindVertexArray, GLuint array); \
GLE(GLuint, CreateShader, GLenum shaderType); \
GLE(void, ShaderSource, GLuint shader, GLsizei count, const GLchar** string, const GLint* length); \
GLE(void, CompileShader, GLuint shader); \
// Other functions you may want to load...

// This takes the above list and defines function pointers for each item
#define GLE(ret, name, ...) typedef ret name##proc(__VA_ARGS__); static name##proc * gl##name;
OPENGL_LIST
#undef GLE

NOTE: Don't include spaces between your lines unless you also include a "" or the macro will terminate before you expect it to. Also, OpenGL functions introduced in version 1.1 are already loaded through the relevant OpenGL headers.

I needed the following piece of code because of how I set up my project (some of my code was split off into separate DLLs). This may be entirely optional for you. If you get errors along the lines of "openGL function X redefinition error" go ahead and remove this code.

#define GLE(ret, name, ...) ret (*gl##name)(__VA_ARGS__);
	OPENGL_LIST
#undef GLE

Creating an OpenGL Context

We have the function pointers ready, but before we can load them, we must have an OpenGL context with which to do so. Specifying the exact version that you want is trickier than using a library. Because in order to do so you must first create a context without a version number and then use that unversioned context to create a new context with the version number you specified. (A library would otherwise do this for you behind the scenes.) You will also need to define a pixel format to give to OpenGL.

void win32_initOpenGL(HWND window) {
	HDC windowDC = GetDC(window);

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

	int suggestedPixelFormatIndex = ChoosePixelFormat(windowDC, &desiredPixelFormat);
	PIXELFORMATDESCRIPTOR suggestedPixelFormat;
	DescribePixelFormat(windowDC, suggestedPixelFormatIndex, sizeof(suggestedPixelFormat), &suggestedPixelFormat);
	SetPixelFormat(windowDC, suggestedPixelFormatIndex, &suggestedPixelFormat);

	HGLRC legacyContext = wglCreateContext(windowDC);
	if (!legacyContext){
		int error = glGetError();
		printf("ERROR: wglCreateContext failed with code %d", error);
		return;
	}

	if (!wglMakeCurrent(windowDC, legacyContext)) {
		int error = glGetError();
		printf("ERROR: wglMakeCurrent failed with code %d", error);
	}

	int flags = WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB;
#if _DEBUG
	flags |= WGL_CONTEXT_DEBUG_BIT_ARB;
#endif

	const int contextAttributes[] =
	{
		WGL_CONTEXT_MAJOR_VERSION_ARB, 4,
		WGL_CONTEXT_MINOR_VERSION_ARB, 3,
		WGL_CONTEXT_FLAGS_ARB, flags,
		//WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
		WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
		0
	};

	PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB
		= (PFNWGLCREATECONTEXTATTRIBSARBPROC)wglGetProcAddress("wglCreateContextAttribsARB");
	if (!wglCreateContextAttribsARB){
		printf("ERROR: Failed querying entry point for wglCreateContextAttribsARB!");
		return;
	}

	HGLRC renderingContext = wglCreateContextAttribsARB(windowDC, 0, contextAttributes);
	if (!renderingContext){
		int error = glGetError();
		printf("ERROR: Couldn't create rendering context! Error code is: %d", error);
		return;
	}
	
	// Destroy dummy context
	BOOL res;
	res = wglMakeCurrent(windowDC, NULL);
	res = wglDeleteContext(legacyContext);
	
	if (!wglMakeCurrent(windowDC, renderingContext)){
		int error = glGetError();
		printf("ERROR: wglMakeCurrent failed with code %d", error);
		return;
	}

	ReleaseDC(window, windowDC);

// Loading the actual functions covered in the next section

Loading the OpenGL functions

Thankfully, the last part is both straightforward and short. We're going to include one last macro to reach into the OpenGL dll and assign our pointers to the functions that we want.

HMODULE module = LoadLibraryA("opengl32.dll");

#define GLE(ret, name, ...) gl##name = (name##proc *) wglGetProcAddress("gl" #name); \
	if(gl##name == 0){ gl##name = (name##proc *) GetProcAddress(module, "gl" #name); } \
	if(gl##name == 0){ printf("%s didn't load\n", "gl" #name); } \
	else{ printf("%s loaded successfully\n", "gl" #name); \
	OPENGL_LIST
#undef GLE

I included some extra print statements so that you can see if they were loaded or not successfully. I found them useful for initial setup to make sure I didn't mess anything up. I removed them once I was confident in that part.

And there you have it! It was much simpler than I thought it would be given how many tutorials avoided it. As a last reminder, I don't think libraries GLEW and GLFW are inherently bad. They might be the right fit for you, but now you know how to setup OpenGL without them so that you can make a more informed decision about the tradeoffs.

I provided a demo project to get you started. I did not provide any actual OpenGL implementation code. There are several great tutorials online. I happen to like this one.