Last time I tried SDL and Dear ImGui and that was quite interesting. So today I would like to explore a GLFW and Dear ImGui combo.

GLFW, Dear ImGui on Mac OS

There will be also a silly SDL vs GLFW comparison.

GLFW is almost the same thing as SDL - a way to create windows and OpenGL contexts. It also seems to have most of the SDL features as well, such as events, user input with keyboard and mouse, and others.

So let’s create a cross-platform application using GLFW and add a Dear ImGui to it.

CMakeLists.txt

It looks like I switched to CMake for good, so I’ll be using it for all my C++ projects, including this one.

GLFW

If you’re on Mac OS or Windows, simply download the pre-build binaries.

If you’re on Linux, then you can try to install it via your package manager, but for me that option failed, so I built it from sources (which can be downloaded from the same page):

$ cd ~/Downloads/glfw-3.3.2
$ mkdir build && cd $_
$ nano ../CMakeLists.txt
// you might want to change install path to something else
set(CMAKE_INSTALL_PREFIX "/home/YOUR-NAME/programs/glfw")

$ cmake -DCMAKE_BUILD_TYPE=Release ..

If that fails with the following error:

CMake Error at CMakeLists.txt:208 (message):
  RandR headers not found; install libxrandr development package

Install these packages and try again:

$ sudo apt install xorg-dev libglu1-mesa-dev

$ cmake -DCMAKE_BUILD_TYPE=Release ..
$ cmake --build . --target install
...
Install the project...
-- Install configuration: "Release"
-- Installing: /home/YOUR-NAME/programs/glfw/include/GLFW
-- Installing: /home/YOUR-NAME/programs/glfw/include/GLFW/glfw3native.h
-- Installing: /home/YOUR-NAME/programs/glfw/include/GLFW/glfw3.h
-- Installing: /home/YOUR-NAME/programs/glfw/lib/cmake/glfw3/glfw3Config.cmake
-- Installing: /home/YOUR-NAME/programs/glfw/lib/cmake/glfw3/glfw3ConfigVersion.cmake
-- Installing: /home/YOUR-NAME/programs/glfw/lib/cmake/glfw3/glfw3Targets.cmake
-- Installing: /home/YOUR-NAME/programs/glfw/lib/cmake/glfw3/glfw3Targets-noconfig.cmake
-- Installing: /home/YOUR-NAME/programs/glfw/lib/pkgconfig/glfw3.pc
-- Installing: /home/YOUR-NAME/programs/glfw/lib/libglfw3.a

Now back to your project. Add GLFW library and its headers:

find_library(GLFW glfw3 PATHS "/home/YOUR-NAME/programs/glfw/lib")

include_directories(
    "/home/YOUR-NAME/programs/glfw/include"
    )

# ...

target_link_libraries(${CMAKE_PROJECT_NAME}
    ${GLFW}
    )

glad

Once again, in order to be able to use the latest OpenGL version, you will need an OpenGL loader library, one of which is glad.

I already described how to get it in the previous article about SDL.

Dear ImGui

Like the last time Dear ImGui source code files are added as they are:

set(sources
    main.cpp
    functions.cpp
    imgui/imconfig.h
    imgui/imgui_demo.cpp
    imgui/imgui_draw.cpp
    imgui/imgui_impl_glfw.cpp
    imgui/imgui_impl_glfw.h
    imgui/imgui_impl_opengl3.cpp
    imgui/imgui_impl_opengl3.h
    imgui/imgui_internal.h
    imgui/imgui_widgets.cpp
    imgui/imgui.cpp
    imgui/imgui.h
    imgui/imstb_rectpack.h
    imgui/imstb_textedit.h
    imgui/imstb_truetype.h
    )

The only difference is that this time instead of SDL implementation we’re using GLFW implementation.

System libraries

On all the platforms you’ll need the OpenGL package:

find_package(OpenGL REQUIRED)

# ...

target_link_libraries(${CMAKE_PROJECT_NAME}
    ${OPENGL_gl_LIBRARY}
    )

Windows

Nothing else is required on Windows. Seriously, it’s not the first time when some development-related thing goes smoothly on Windows, while on other platforms it is accompanied with all sorts of brainfuck. Makes you think, you know.

Well, actually there was one thing. At first building was failing with complains about main() function not being WinMain(), which I resolved with the following instruction:

set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")

But when I removed it later at some point, nothing changed - there were no more complains and it all worked fine with main().

Mac OS

On Mac OS the following frameworks are required:

target_link_libraries(${CMAKE_PROJECT_NAME}
    "-framework Cocoa"
    "-framework IOKit"
    "-framework CoreVideo"
    )

Without it you’ll get a kilometer-long list of linking errors like this:

[build] Undefined symbols for architecture x86_64:
[build]   "_CFArrayAppendValue", referenced from:
[build]       __glfwInitJoysticksNS in libglfw3.a(cocoa_joystick.m.o)
[build]       _matchCallback in libglfw3.a(cocoa_joystick.m.o)

Linux

On Linux you’ll need the following packages:

find_package(Threads REQUIRED)
find_package(X11 REQUIRED)

# ...

target_link_libraries(${CMAKE_PROJECT_NAME}
    ${CMAKE_THREAD_LIBS_INIT}
    ${X11_LIBRARIES}
    ${CMAKE_DL_LIBS}
    )

If you won’t add those, you’ll get these linking errors:

[build] /usr/bin/ld: libglfw3.a(posix_thread.c.o): undefined reference to symbol 'pthread_key_delete@@GLIBC_2.2.5'
[build] //lib/x86_64-linux-gnu/libpthread.so.0: error adding symbols: DSO missing from command line

[build] /usr/bin/ld: libglfw3.a(x11_window.c.o): undefined reference to symbol 'XConvertSelection'
[build] //usr/lib/x86_64-linux-gnu/libX11.so.6: error adding symbols: DSO missing from command line

[build] /usr/bin/ld: libglad.a(glad.c.o): undefined reference to symbol 'dlclose@@GLIBC_2.2.5'
[build] //lib/x86_64-linux-gnu/libdl.so.2: error adding symbols: DSO missing from command line

You can find the full CMakeLists.txt here.

Application

A simple window

Here’s how you get a basic window with GLFW:

// C++
#include <string>
#include <iostream>
// GLFW
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include "functions.h"

std::string programName = "GLFW window";
int windowWidth = 1200,
    windowHeight = 800;
float backgroundR = 0.1f,
      backgroundG = 0.3f,
      backgroundB = 0.2f;

static void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
    glViewport(0, 0, width, height);
}

void teardown(GLFWwindow *window)
{
    if (window != NULL) { glfwDestroyWindow(window); }
    glfwTerminate();
}

int main(int argc, char *argv[])
{
    if (!glfwInit())
    {
        std::cerr << "[ERROR] Couldn't initialize GLFW\n";
        return -1;
    }
    else
    {
        std::cout << "[INFO] GLFW initialized\n";
    }

    // setup GLFW window

    glfwWindowHint(GLFW_DOUBLEBUFFER , 1);
    glfwWindowHint(GLFW_DEPTH_BITS, 24);
    glfwWindowHint(GLFW_STENCIL_BITS, 8);

    glfwWindowHint(
        GLFW_OPENGL_PROFILE,
        GLFW_OPENGL_CORE_PROFILE
        );

    std::string glsl_version = "";
#ifdef __APPLE__
    // GL 3.2 + GLSL 150
    glsl_version = "#version 150";
    glfwWindowHint( // required on Mac OS
        GLFW_OPENGL_FORWARD_COMPAT,
        GL_TRUE
        );
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
#elif __linux__
    // GL 3.2 + GLSL 150
    glsl_version = "#version 150";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
#elif _WIN32
    // GL 3.0 + GLSL 130
    glsl_version = "#version 130";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
#endif

    float highDPIscaleFactor = 1.0;
#ifdef _WIN32
    // if it's a HighDPI monitor, try to scale everything
    GLFWmonitor *monitor = glfwGetPrimaryMonitor();
    float xscale, yscale;
    glfwGetMonitorContentScale(monitor, &xscale, &yscale);
    if (xscale > 1 || yscale > 1)
    {
        highDPIscaleFactor = xscale;
        glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
    }
#elif __APPLE__
    // to prevent 1200x800 from becoming 2400x1600
    glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE);
#endif

    GLFWwindow *window = glfwCreateWindow(
        windowWidth,
        windowHeight,
        programName.c_str(),
        NULL,
        NULL
        );
    if (!window)
    {
        std::cerr << "[ERROR] Couldn't create a GLFW window\n";
        teardown(NULL);
        return -1;
    }
    // watch window resizing
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwMakeContextCurrent(window);
    // VSync
    glfwSwapInterval(1);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cerr << "[ERROR] Couldn't initialize GLAD" << std::endl;
        teardown(window);
        return -1;
    }
    else
    {
        std::cout << "[INFO] GLAD initialized\n";
    }

    std::cout << "[INFO] OpenGL from glad "
              << GLVersion.major << "." << GLVersion.minor
              << std::endl;

    int actualWindowWidth, actualWindowHeight;
    glfwGetWindowSize(window, &actualWindowWidth, &actualWindowHeight);
    glViewport(0, 0, actualWindowWidth, actualWindowHeight);

    glClearColor(backgroundR, backgroundG, backgroundB, 1.0f);
    // --- rendering loop
    while (!glfwWindowShouldClose(window))
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    teardown(window);

    return 0;
}

Running this on Mac OS gives this simple window:

GLFW window on Mac OS

And the console output:

[INFO] GLFW initialized
[INFO] GLAD initialized
[INFO] OpenGL from glad 4.1

High DPI

What’s interesting here is the way how GLFW allows you to handle High DPI / Retina displays - you can set so-called window creation hints before creating a window.

For example, on Mac OS you can use GLFW_COCOA_RETINA_FRAMEBUFFER hint to prevent your window from getting values twice as big in pixels:

glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE);

And on Windows it’s the other way around - if your window is too damn small, then you might want to set GLFW_SCALE_TO_MONITOR hint:

glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);

As you can see I also defined a highDPIscaleFactor variable here for later use.

Adding Dear ImGui

Rendering GUI with Dear ImGui in a GLFW window is not so different from doing the same with SDL, so I won’t place the entire code for it here, you can take a look at it later in the repository. I based my code on this example.

Mostly, the only differences would be GLFW-related Dear ImGui implementations, which come instead of SDL:

//#include "imgui/imgui_impl_sdl.h"
#include "imgui/imgui_impl_glfw.h"

// ...

//ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version.c_str());

// ...

//ImGui_ImplSDL2_NewFrame(window);
ImGui_ImplGlfw_NewFrame();

Note that I used an OpenGL 3.x (imgui/imgui_impl_opengl3.h) implementation here. This is because documentation recommends not to use OpenGL 2.x (imgui/imgui_impl_opengl2.h) implementation in actual applications and take the OpenGL 3.x instead. I can only second this suggestion as while this example worked just fine, I ran into certain problems with my other application, and I was able to resolve those only after switching to OpenGL 3.x implementation.

On High DPI displays you might want to utilize highDPIscaleFactor variable that we set earlier - to scale all the paddings/margins/spacings:

void setImGuiStyle(float highDPIscaleFactor)
{
    ImGuiStyle &style = ImGui::GetStyle();

    // ...

    style.ScaleAllSizes(highDPIscaleFactor);
}

and also the font sizes:

io.Fonts->AddFontFromFileTTF("verdana.ttf", 18.0f * highDPIscaleFactor, NULL, NULL);

It’s cross-platform

Since it’s C++ and thanks to our universal CMakeLists.txt, the application builds and works fine not only on Mac OS but also on Windows:

GLFW and Dear ImGui on Windows

and on Linux:

GLFW and Dear ImGui on Linux

The full project source code is published here.

GLFW vs SDL

I cannot really compare GLFW with SDL thoroughly, because I don’t know much about their capabilities, but I was curious to compare the footprint of applications built with both.

Mac OS

Binary size:

  • SDL-based: 693 KB;
  • GLFW-based: 1.3 MB.

CPU and memory usage:

GLFW vs SDL, CPU and memory usage on Mac OS

Linux

Binary size:

  • SDL-based: 731 KB;
  • GLFW-based: 1.4 MB.

CPU and memory usage:

GLFW vs SDL, CPU and memory usage on Linux

Windows

Binary size:

  • SDL-based: 506 KB (plus 1.26 MB of SDL2.dll, which should be less in case of static linking);
  • GLFW-based: 882 KB.

CPU and memory usage:

GLFW vs SDL, CPU and memory usage on Windows

Updates

22.01.2022

The article got somewhat obsolete, as the reference application has been updated over the time (starting with this commit):

  • CMake project file is better structured now;
  • initialization of GLFW and Dear ImGui was moved to functions;
    • same for composing the Dear ImGui frame;
  • dependencies can be resolved with Conan (more details here);
  • other smaller changes and improvements.

Also, thanks to this great article, the scene finally has some content (a triangle):

GLFW, Dear ImGui and a triangle on Mac OS