Resizing the window 🟑¢

Resulting code: step085

Resulting code: step085-vanilla

When we introduced the swap chain, we prevented the window from resizing by setting the GLFW_RESIZABLE window β€œhint” to false, because the swap chained is tied to a specific size.

Now that our code is better organized, it becomes easy to get rid of this limitation: in this chapter, we restore the possibility to resize the window:

glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);

To do so, we add a onResize handler that we call when the window is resized, and rebuild the swap chain with the new resolution. And we also need to resize the depth buffer by the way.

Callback setupΒΆ

Let us first add the onResize() method:

class Application {
public:
    // A function called when the window is resized.
    void onResize();
}

GLFW provides a mechanism for setting a callback to be invoked each time the window size changes: glfwSetFramebufferSizeCallback. Naively, we could consider the following:

// DON'T (this will not work)
bool Application::initWindowAndDevice() {
    // [...]

    // Add window callbacks
    glfwSetFramebufferSizeCallback(m_window, Application::onResize);

    return /* [...] */;
}

This cannot work because a non-static class method like Application::onResize needs a value of this when it is called, so it cannot be used as a function pointer (which glfwSetFramebufferSizeCallback expects).

The usual trick here is to define a simple callback function that does nothing but to call the onResize() method.

πŸ€” But how do I specify β€œthis”? Should I use a global variable?

It is precisely for this pattern that GLFW provides a user pointer, namely an arbitrary value that can be associated to a window. Since the callback receives the window as first argument, we can get the user pointer back and use it:

// The raw GLFW callback, that must always have the same signature
// even though we do not use the 'width' and 'height' arguments here.
void onWindowResize(GLFWwindow* window, int /* width */, int /* height */) {
    // We know that even though from GLFW's point of view this is
    // "just a pointer", in our case it is always a pointer to an
    // instance of the class `Application`
    auto that = reinterpret_cast<Application*>(glfwGetWindowUserPointer(window));

    // Call the actual class-member callback
    if (that != nullptr) that->onResize();
}

bool Application::initWindowAndDevice() {
    // [...]

    // Set the user pointer to be "this"
    glfwSetWindowUserPointer(m_window, this);
    // Add the raw `onWindowResize` as resize callback
    glfwSetFramebufferSizeCallback(m_window, onWindowResize);

    return /* [...] */;
}

If we want to make this more compact and avoid creating a dedicated function, we can use a lambda when calling glfwSetFramebufferSizeCallback:

bool Application::initWindowAndDevice() {
    // [...]

    // Set the user pointer to be "this"
    glfwSetWindowUserPointer(m_window, this);
    // Use a non-capturing lambda as resize callback
    glfwSetFramebufferSizeCallback(m_window, [](GLFWwindow* window, int, int){
        auto that = reinterpret_cast<Application*>(glfwGetWindowUserPointer(window));
        if (that != nullptr) that->onResize();
    });

    return /* [...] */;
}

Important

I did not show the lambda version right away because it is slightly misleading: it is tempting to use the capturing context of the lambda (the [] before the lambda’s arguments) to provide this to the callback.

However, only non-capturing lambdas may be casted to the raw function pointer that GLFW expects for a callback. This remark goes for any C API by the way.

Resize event handlerΒΆ

Swap Chain and Depth BufferΒΆ

With our new design, the content of onResize() is pretty simple:

void Application::onResize() {
    // Terminate in reverse order
    terminateDepthBuffer();
    terminateSwapChain();

    // Re-init
    initSwapChain();
    initDepthBuffer();
}

Note

On this simple example, managing the lifetime of WebGPU objects (e.g., destroying before rebuild them, releasing at the end of the application, etc.) can be done manually. But since this is in general quite error-prone, the RAII chapter presents a common C++ design pattern that makes this easier.

However, we need to update initSwapChain() and initDepthBuffer() to take into account the actual size of the window, rather than the hardcoded \((640,480)\).

bool Application::initSwapChain() {
    // Get the current size of the window's framebuffer:
    int width, height;
    glfwGetFramebufferSize(m_window, &width, &height);

    // [...]
    swapChainDesc.width = static_cast<uint32_t>(width);
    swapChainDesc.height = static_cast<uint32_t>(height);
    // [...]
}

bool Application::initDepthBuffer() {
    // Get the current size of the window's framebuffer:
    int width, height;
    glfwGetFramebufferSize(m_window, &width, &height);

    // [...]
    depthTextureDesc.size = { static_cast<uint32_t>(width), static_cast<uint32_t>(height), 1 };
    // [...]
}

Camera ProjectionΒΆ

If you look for β€œ640” in your code to ensure nothing relies any more on the original window size, you’ll find that we use the size when defining the camera projection. Thus the onResize function must also update the projection matrix uniform:

void Application::onResize() {
    // [...] Rebuild swap chain and depth buffer
    updateProjectionMatrix();
}

Where updateProjectionMatrix is a new private method:

void Application::updateProjectionMatrix() {
    int width, height;
    glfwGetFramebufferSize(m_window, &width, &height);
    float ratio = width / (float)height;
    m_uniforms.projectionMatrix = glm::perspective(45 * PI / 180, ratio, 0.01f, 100.0f);
    m_queue.writeBuffer(
        m_uniformBuffer,
        offsetof(MyUniforms, projectionMatrix),
        &m_uniforms.projectionMatrix,
        sizeof(MyUniforms::projectionMatrix)
    );
}
void Application::updateProjectionMatrix() {
    int width, height;
    glfwGetFramebufferSize(m_window, &width, &height);
    float ratio = width / (float)height;
    m_uniforms.projectionMatrix = glm::perspective(45 * PI / 180, ratio, 0.01f, 100.0f);
    wgpuQueueWriteBuffer(
        m_queue,
        m_uniformBuffer,
        offsetof(MyUniforms, projectionMatrix),
        &m_uniforms.projectionMatrix,
        sizeof(MyUniforms::projectionMatrix)
    );
}

Note

If your screen is larger than the 2048 limit we set for texture dimensions, you need to use glfwGetMonitors and then glfwGetMonitorWorkarea when initializing WebGPU limits to set them to at least the size of your biggest monitor!

ConclusionΒΆ

This pattern that we used to connect the raw GLFW resize event to our C++ idiomatic object-oriented application skeleton is a commonly used pattern: we will do the same for all the other interaction callbacks we need. We see in the next chapter the case of mouse button and mouse move callbacks to get the camera controller!

Resulting code: step085

Resulting code: step085-vanilla