Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 48

Native Window Management Boilerplate

Recommended resources

Hello! Welcome back to our project.

In our last lesson, we laid the critical groundwork for our telemetry engine's user interface. You configured a modern CMake project to automatically fetch and link the necessary libraries for windowing (GLFW) and graphics (OpenGL/GLAD). Right now, you have a project that can successfully compile a main.cpp file that includes the GLFW and GLAD headers, but it doesn't do anything visible yet.

Today, we bring that project to life. Our goal is to write the boilerplate code to create and manage a native window. This window will serve as the canvas for our entire visualization suite. We will write the C++ code that tells the operating system to open a window, keep it running, and respond to user input, like clicking the close button.


The Anatomy of a GLFW Application

Before we dive into the code, let's look at the lifecycle of a typical application that uses GLFW. Understanding this sequence is key, as you will write this pattern for almost any native graphical application.

The process follows these distinct steps:

  1. Initialize GLFW: Start the library.
  2. Configure Window Hints: Tell GLFW what kind of window and OpenGL context we want (e.g., which version of OpenGL to use).
  3. Create Window: Create the actual window object and its associated OpenGL context.
  4. Make Context Current: Tell the operating system that all subsequent OpenGL commands should apply to this new window.
  5. Load OpenGL Functions: (We'll do this in the next lesson with GLAD).
  6. Run the Render Loop: A while loop that runs every frame, keeping the window open. Inside this loop, we:
    • Process user input (like closing the window).
    • Perform rendering (which will be a blank screen for now).
    • Swap the front and back buffers to display the rendered image.
  7. Terminate GLFW: Clean up all allocated resources before the program exits.
OpenGL + GLFW Window Creation and Game Loop
The core of a graphical application is a loop that continuously processes input, updates state, and renders the result. We'll build this structure step-by-step.

Implementing the Window Boilerplate

We'll now translate those steps into C++ code. The process involves calling a series of functions from the GLFW library in a specific order.

The following video provides a great visual walkthrough of this exact process. We will go through it section by section.

OpenGL Tutorial 1 - Window

Watch this tutorial by Victor Gordan to see the fundamental steps of creating a window with GLFW. We will break it down piece by piece.

Watch the entire video (around 4 minutes). Pay close attention to the sequence of function calls: glfwInit, glfwWindowHint, glfwCreateWindow, glfwMakeContextCurrent, and the structure of the while loop.

Now, let's break down the code you saw in the video and reconstruct it for our main.cpp.

1. Initialization and Configuration

First, we must initialize the GLFW library. We also need to give it "hints" about the OpenGL context we want to create. For our project, we'll use OpenGL 3.3 and the "Core" profile, which gives us access to modern OpenGL features without legacy functions.

#include <iostream>
#include <GLFW/glfw3.h> // Will be found by CMake

int main() {
    // 1. Initialize GLFW
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return -1;
    }

    // 2. Set window hints for OpenGL 3.3 Core Profile
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // ... rest of the code will go here ...

    // Last step: Terminate GLFW
    glfwTerminate();
    return 0;
}

Notice that we immediately write glfwTerminate() at the end. This is good practice to ensure resources are always cleaned up.

2. Creating the Window and Context

Next, we ask GLFW to create the window. This function returns a pointer to a GLFWwindow object, which is an opaque handle to our window. We must check if the pointer is NULL (or nullptr in modern C++), as window creation can fail if the user's system doesn't support the requested OpenGL version.

After successfully creating the window, we must tell GLFW to make its associated OpenGL context the "current" one for this thread. An OpenGL context is essentially a state machine that holds all the data for rendering (shaders, textures, etc.). Any OpenGL calls we make will affect the currently active context.

// Add this after setting window hints

// 3. Create the window
GLFWwindow* window = glfwCreateWindow(1280, 720, "Telemetry Engine", NULL, NULL);
if (window == nullptr) {
    std::cerr << "Failed to create GLFW window" << std::endl;
    glfwTerminate();
    return -1;
}

// 4. Make the window's context current
glfwMakeContextCurrent(window);

3. The Render Loop

If you were to run the code now, a window would flash on the screen and disappear instantly. That's because the main function would finish executing. To keep the window open, we need a loop that runs until we tell it to stop.

This is the render loop.

  • The loop condition is !glfwWindowShouldClose(window). This function checks a flag that GLFW sets to true when the user clicks the window's close button.
  • Inside the loop, glfwPollEvents() processes any pending events, such as keyboard input, mouse movement, or the close request itself. Without this, the window would be unresponsive.
  • glfwSwapBuffers(window) performs the buffer swap. Modern graphics use a technique called double buffering to prevent flickering. All rendering commands draw to an off-screen "back" buffer. When rendering for the frame is complete, glfwSwapBuffers presents this finished image to the screen by swapping it with the "front" buffer.
// Add this after making the context current

// 5. The Render Loop
while (!glfwWindowShouldClose(window)) {
    // --- Input ---
    // (We will add input processing here later)
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true);
    }

    // --- Rendering ---
    // (We will add rendering commands here later)

    // --- Swapping buffers and polling events ---
    glfwSwapBuffers(window);
    glfwPollEvents();
}

I've added a small check for the Escape key to provide another way to close the window, which is useful for testing.


Putting It All Together

The complete main.cpp file combines all these steps into a functional boilerplate for a graphical application. The learnopengl.com article "Hello Window" provides an excellent textual reference that consolidates everything we've discussed.

Hello Window

To see these concepts explained in a comprehensive text format, please read the 'Hello Window' chapter from learnopengl.com. It covers the exact same lifecycle and provides clear code examples.

Read through the page, focusing on the code snippets and explanations for glfwInit, glfwWindowHint, glfwCreateWindow, glfwMakeContextCurrent, the while loop with glfwPollEvents and glfwSwapBuffers, and finally glfwTerminate. This will solidify your understanding of the code we just built.

Here is the final main.cpp file for you to use in your project. Replace the contents of your current main.cpp with this code, build, and run it. You should see a 1280x720 window with a black background that you can close with the 'X' button or the Escape key.

#include <iostream>

// Make sure GLAD is included before GLFW
// We haven't initialized GLAD yet, but the headers need to be in the right order.
// #include <glad/glad.h> 
#include <GLFW/glfw3.h>

int main() {
    // Initialize GLFW
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return -1;
    }

    // Set window hints for OpenGL 3.3 Core Profile
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    // Required for macOS
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // Create the window
    GLFWwindow* window = glfwCreateWindow(1280, 720, "Telemetry Engine", NULL, NULL);
    if (window == nullptr) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }

    // Make the window's context current
    glfwMakeContextCurrent(window);

    // The Render Loop
    while (!glfwWindowShouldClose(window)) {
        // Input
        if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
            glfwSetWindowShouldClose(window, true);
        }

        // Rendering commands will go here...
        // For now, we will clear the screen in the next lesson.

        // Swap buffers and poll for IO events
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // Cleanup
    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

Note: I have commented out the GLAD include for now, as we haven't configured it yet. We will uncomment it in the next lesson.

Test your understanding!

What would happen if you moved glfwPollEvents() to be the first line inside the while loop, before glfwSwapBuffers()? Would the application still work correctly? Why or why not?

Show answer

Yes, the application would still work correctly. The order of glfwPollEvents() and glfwSwapBuffers() inside the loop doesn't matter for basic functionality. glfwPollEvents() processes input, and glfwSwapBuffers() displays the rendered frame. Doing one before the other just changes whether you process the current frame's input before or after you show the last frame's image. In more complex applications with high-frequency input, this order can have subtle effects on input latency, but for our purposes, either order is fine. The crucial part is that both are called once per frame.



Conclusion

Congratulations! You've successfully created a native C++ application that opens and manages a window. This is a fundamental skill and a huge step towards building our visualization front-end.

Key Takeaways:

  • A graphical application follows a clear lifecycle: Initialize -> Configure -> Create -> Loop -> Terminate.
  • glfwInit() starts the library and glfwTerminate() cleans it up.
  • glfwCreateWindow() creates the window and an OpenGL context.
  • The render loop (while !glfwWindowShouldClose) is the heart of the application, keeping it alive frame by frame.
  • glfwPollEvents() is essential for responsiveness and processing input.
  • glfwSwapBuffers() uses double buffering to present a smooth, flicker-free image.

What's Next?

We have a window and an OpenGL context, but we can't actually use any OpenGL functions yet. Our application doesn't know where to find them in the graphics driver. In the next lesson, you will initialize a graphics context using an API loader (GLAD). This will unlock the full power of OpenGL, allowing us to clear the screen to a specific color and, most importantly, prepare the ground for integrating our first ImGui widgets.