An OpenGL Window

Getting OpenGL running on Windows can feel a bit strange at first. When you create an OpenGL context, you initially get a legacy 1.0 context, which in older systems (Windows XP era) might have used a software renderer. So how do you get a modern 3.3 or 4.x OpenGL context? The short answer: use OpenGL's extension mechanism. You first create a temporary window with a legacy context, load the required "ARB" extension functions, destroy that legacy context, and finally create a window with a modern OpenGL context.

Once you have a modern context, you still need to load the actual OpenGL function pointers. In this blog, we'll use GLAD as our OpenGL loader to avoid manually calling wglGetProcAddress for every function.

Let's walk through the process of creating a modern OpenGL window on Windows, step by step. We also take advantage of "high DPI" support, which means including <code>ShellScalingAPI.h</code> and making our process DPI aware.

First, we include our headers and define some simple window parameters:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <ShellScalingAPI.h> // Shcore.lib
#include "glad.h" // Must link OpenGL32.lib

#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
#define WINDOW_CLASS "OGL_Sample"
#define WINDOW_TITLE "OpenGL Sample"

We use WINDOW_WIDTH and WINDOW_HEIGHT as the initial client area of our window. WINDOW_CLASS and WINDOW_TITLE are used to specify the window class name and the window's title bar text. We also define WIN32_LEAN_AND_MEAN to trim some of the less-used parts of Windows headers, making compilation faster.

Next, we set up constants and function pointer types for creating a modern OpenGL context. These come from the ARB (Architecture Review Board) extensions that let us pick more advanced pixel formats and context attributes.

// Used for setting up a modern OpenGL context
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC) (
    HDC hDC, HGLRC hShareContext, const int* attribList
);
typedef BOOL (WINAPI* PFNWGLCHOOSEPIXELFORMATARBPROC)(
    HDC hdc,
    const int* piAttribIList,
    const FLOAT* pfAttribFList,
    UINT nMaxFormats,
    int* piFormats,
    UINT* nNumFormats
);

// ARB context creation constants
#define WGL_CONTEXT_MAJOR_VERSION_ARB            0x2091
#define WGL_CONTEXT_MINOR_VERSION_ARB            0x2092
#define WGL_CONTEXT_LAYER_PLANE_ARB              0x2093
#define WGL_CONTEXT_FLAGS_ARB                    0x2094
#define WGL_CONTEXT_PROFILE_MASK_ARB             0x9126
#define WGL_CONTEXT_DEBUG_BIT_ARB                0x0001
#define WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB   0x0002
#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB         0x00000001
#define WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB 0x00000002
#define WGL_DRAW_TO_WINDOW_ARB                   0x2001
#define WGL_SUPPORT_OPENGL_ARB                   0x2010
#define WGL_DOUBLE_BUFFER_ARB                    0x2011
#define WGL_PIXEL_TYPE_ARB                       0x2013
#define WGL_COLOR_BITS_ARB                       0x2014
#define WGL_DEPTH_BITS_ARB                       0x2022
#define WGL_STENCIL_BITS_ARB                     0x2023
#define WGL_TYPE_RGBA_ARB                        0x202B
#define WGL_SAMPLE_BUFFERS_ARB                   0x2041
#define WGL_SAMPLES_ARB                          0x2042

Then, let's define a small WindowData struct to hold our Win32 handles (window, device context, OpenGL rendering context, etc.), and forward-declare some lifecycle functions we'll implement later:

struct WindowData {
    HWND hwnd;
    HDC hdc;
    HGLRC hglrc;
    HINSTANCE hInstance;
    bool closeWindow;
};

void* Initialize(const WindowData& windowData);
void Update(void* userData, float deltatTime);
void Render(void* userData, RECT clientRect);
void Shutdown(void* userData);

Now we set up our entry points. We'll define a WinMain function (the typical Windows application entry) and also a main that simply forwards to WinMain for convenience. We also define WndProc, the callback for processing Windows messages.

HDC gHdc;  // We'll keep a global HDC for simplicity

LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow);

int main(int argc, char** argv) {
    return WinMain(GetModuleHandleA(NULL), NULL, GetCommandLineA(), SW_SHOWDEFAULT);
}

Inside WinMain, we first make our process DPI aware to handle high DPI displays correctly. Then we create two windows: a temporary one for getting a legacy OpenGL context (so we can load the ARB extensions), and then a "real" one with a modern OpenGL context. The code below is split into chunks for clarity.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) {
    // Make process DPI aware so our window isn't scaled incorrectly on high DPI displays
    SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
    float dpi = (float)GetDpiForSystem() / 96.0f;

    // We'll define two window classes:
    // 1) A 'dummy' class for the temp window (to get the legacy GL context).
    // 2) Our real class that we'll use for the final OpenGL window.
    WNDCLASSA wndclass, tmpclass;
    ZeroMemory(&wndclass, sizeof(WNDCLASSA));
    ZeroMemory(&tmpclass, sizeof(WNDCLASSA));

    // Setup both classes similarly
    tmpclass.style = wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    tmpclass.lpfnWndProc = DefWindowProcA;
    tmpclass.hInstance = wndclass.hInstance = hInstance;
    tmpclass.hIcon = wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    tmpclass.hCursor = wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    tmpclass.hbrBackground = wndclass.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);

    // Our real window class name, and a separate dummy class name
    wndclass.lpszClassName = WINDOW_CLASS;
    tmpclass.lpszClassName = "tmp_gl_context_window";

    RegisterClassA(&wndclass);
    RegisterClassA(&tmpclass);

    // Determine the window size
    int windowWidth = (int)((float)WINDOW_WIDTH * dpi);
    int windowHeight = (int)((float)WINDOW_HEIGHT * dpi);

    RECT rClient;
    SetRect(&rClient, 0, 0, windowWidth, windowHeight);
    AdjustWindowRect(&rClient, WS_OVERLAPPEDWINDOW | WS_VISIBLE, FALSE);

    int screenWidth = GetSystemMetrics(SM_CXSCREEN);
    int screenHeight = GetSystemMetrics(SM_CYSCREEN);
    windowWidth = rClient.right - rClient.left;
    // Fix bug: the next line should calculate height from 'bottom - top'
    windowHeight = rClient.bottom - rClient.top;

    // --- Create a DUMMY window to get a legacy GL context ---
    HWND hwnd = CreateWindowA(
        tmpclass.lpszClassName,
        (char*)(WINDOW_TITLE),
        WS_OVERLAPPEDWINDOW,
        (screenWidth / 2) - (windowWidth / 2),
        (screenHeight / 2) - (windowHeight / 2),
        windowWidth,
        windowHeight,
        NULL, NULL, hInstance, 0
    );

    // Temporary window's device context
    HDC hdc = GetDC(hwnd);

    // Create a PIXELFORMATDESCRIPTOR for a legacy context
    PIXELFORMATDESCRIPTOR pfd;
    ZeroMemory(&pfd, sizeof(PIXELFORMATDESCRIPTOR));
    pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
    pfd.nVersion = 1;
    pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 24;
    pfd.cDepthBits = 32;
    pfd.cStencilBits = 8;
    pfd.iLayerType = PFD_MAIN_PLANE;

    int pixelFormat = ChoosePixelFormat(hdc, &pfd);
    SetPixelFormat(hdc, pixelFormat, &pfd);

    // Create the legacy context
    HGLRC hglrc = wglCreateContext(hdc);
    wglMakeCurrent(hdc, hglrc);

    // Fetch function pointers for modern context creation
    PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB =
        (PFNWGLCREATECONTEXTATTRIBSARBPROC)wglGetProcAddress("wglCreateContextAttribsARB");
    PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB =
        (PFNWGLCHOOSEPIXELFORMATARBPROC)wglGetProcAddress("wglChoosePixelFormatARB");

    // Done with the dummy (legacy) context
    wglMakeCurrent(hdc, NULL);
    wglDeleteContext(hglrc);
    ReleaseDC(hwnd, hdc);
    DestroyWindow(hwnd);

In the code above, we register two window classes. One is a "dummy" class with a DefWindowProcA so we can create a hidden window with minimal overhead, get a legacy context, then use that context to retrieve the ARB function pointers. After that, we clean up the dummy window entirely.

Now we create the "real" window, using the ARB calls to set up a modern context with multi-sampling support (MSAA) as an example.

    // --- Create the REAL window with a modern GL context ---
    hwnd = CreateWindowA(
        wndclass.lpszClassName,
        (char*)(WINDOW_TITLE),
        WS_OVERLAPPEDWINDOW | WS_VISIBLE,
        (screenWidth / 2) - (windowWidth / 2),
        (screenHeight / 2) - (windowHeight / 2),
        windowWidth,
        windowHeight,
        NULL,
        NULL,
        hInstance,
        0
    );

    hdc = GetDC(hwnd);
    gHdc = hdc;  // store globally if desired

    // We specify our desired pixel format via the attribute list
    const int iPixelFormatAttribList[] = {
        WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
        WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
        WGL_DOUBLE_BUFFER_ARB, GL_TRUE,
        WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
        WGL_COLOR_BITS_ARB, 32,
        WGL_DEPTH_BITS_ARB, 24,
        WGL_STENCIL_BITS_ARB, 8,
        WGL_SAMPLE_BUFFERS_ARB, 1,   // Enable MSAA
        WGL_SAMPLES_ARB, 8,         // 8x MSAA
        0, 0 // End of attributes list
    };

    int nPixelFormat = 0;
    UINT iNumFormats = 0;
    wglChoosePixelFormatARB(hdc, iPixelFormatAttribList, NULL, 1, &nPixelFormat, &iNumFormats);

    // Describe and set the chosen pixel format
    ZeroMemory(&pfd, sizeof(PIXELFORMATDESCRIPTOR));
    DescribePixelFormat(hdc, nPixelFormat, sizeof(PIXELFORMATDESCRIPTOR), &pfd);
    SetPixelFormat(hdc, nPixelFormat, &pfd);

    // Now create the real OpenGL context
    const int attributes[] = {
        WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
        WGL_CONTEXT_MINOR_VERSION_ARB, 2,
        WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
        0 // Terminate attribute list
    };

    hglrc = wglCreateContextAttribsARB(hdc, NULL, attributes);
    wglMakeCurrent(hdc, hglrc);

    // Load all required extensions via GLAD
    if (!gladLoadGL()) {
        exit(-1); // handle init failure
    }
    // OpenGL is now initialized!

With our modern OpenGL context created and set, we can now proceed with the usual loop: initialize user data, run an update-render loop, and finally clean up on exit. Below is the rest of WinMain, split up to explain each part:

    // Store our window/context data in a struct
    WindowData windowData = { 0 };
    windowData.hwnd = hwnd;
    windowData.hglrc = hglrc;
    windowData.hdc = hdc;
    windowData.hInstance = hInstance;
    windowData.closeWindow = false;

    // Attach it to the window for easy retrieval inside WndProc
    LONG_PTR lptr = (LONG_PTR)(&windowData);
    SetWindowLongPtrA(hwnd, GWLP_USERDATA, lptr);

    // Show the window and force an initial paint
    ShowWindow(hwnd, SW_NORMAL);
    UpdateWindow(hwnd);

    // Call user-defined Initialize
    void* userData = Initialize(windowData);

    // Set up a timer for measuring frame times
    MSG msg;
    bool running = true;
    bool quit = false;

    LARGE_INTEGER timerFrequency;
    LARGE_INTEGER thisTick;
    LARGE_INTEGER lastTick;
    LONGLONG timeDelta;

    if (!QueryPerformanceFrequency(&timerFrequency)) {
        OutputDebugStringA("WinMain: QueryPerformanceFrequency failed\n");
    }

    QueryPerformanceCounter(&thisTick);
    lastTick = thisTick;

We keep a message loop for Windows events (close, resize, etc.). If the user closes the window, we handle that in WndProc by setting closeWindow to true. The main loop also calls Update and Render each frame, then finally SwapBuffers to push the rendered image to the display. A small Sleep helps keep CPU usage down in this sample.

    while (!quit) {
        // Process all pending messages
        while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) {
            if (msg.message == WM_QUIT) {
                quit = true;
                break;
            }
            TranslateMessage(&msg);
            DispatchMessageA(&msg);
        }

        // If the user has requested the window to close, shut down
        if (windowData.closeWindow && running) {
            Shutdown(userData);
            running = false;
        }
        else if (running) {
            // Calculate delta time for animation and logic
            RECT clientRect = {};
            GetClientRect(hwnd, &clientRect);

            QueryPerformanceCounter(&thisTick);
            timeDelta = thisTick.QuadPart - lastTick.QuadPart;
            double deltaTime = (double)timeDelta * 1000.0 / (double)timerFrequency.QuadPart;

            Update(userData, (float)deltaTime);
            Render(userData, clientRect);

            lastTick = thisTick;

            // Present the back buffer
            SwapBuffers(hdc);

            // Sleep a bit; we might later rely on vsync instead
            Sleep(1);
        }
    }

    return (int)msg.wParam;
}

The WndProc is mostly straightforward. When the user attempts to close the window, we set closeWindow to true and destroy the window, which leads to a WM_DESTROY message and eventually posts a quit signal.

LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) {
    switch (iMsg) {
    case WM_CLOSE:
    {
        LONG_PTR lptr = GetWindowLongPtrA(hwnd, GWLP_USERDATA);
        WindowData* windowData = (WindowData*)lptr;
        windowData->closeWindow = true;
    }
    DestroyWindow(hwnd);
    return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcA(hwnd, iMsg, wParam, lParam);
}

That's it! We now have a modern OpenGL window running on Windows. If you needed to release your HGLRC and HDC on shutdown, you could do so in Shutdown. From here, you can add any typical OpenGL drawing you like, handle user input, etc.

Once again, the key steps are: create a dummy window and legacy context, load the ARB extension pointers, destroy the dummy context, create your real window with a modern context. After that, load function pointers with gladLoadGL or your preferred loader, and you're ready to go.

Full code listing: https://gist.github.com/gszauer/03fa4d1fbd72bfc169f70e78eabe3c4c