ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
60.22k stars 10.2k forks source link

Weird FPS and delta time behavior on MacBook Pro. #7149

Closed ShaiAvr closed 9 months ago

ShaiAvr commented 9 months ago

Version/Branch of Dear ImGui:

Version: 1.90 WIP (18993) Branch: docking

Back-end/Renderer/Compiler/OS

Back-ends: imgui_impl_glfw.cpp + imgui_impl_opengl3.cpp

My Issue/Question:

I am working on a little graphics engine and wanted to add UI with Dear ImGui. While testing the example of glfw+opengl3 I found a weird problem. I wanted to get a good measure of each frame's time and the FPS. I did it by calling glfwGetTime() at the start and end of the loop to get a delta time and then calculated the FPS as 1 / dt. I also did the calculation using 1 / io.DeltaTime and by averaging the FPS over the last 120 frames like io.FrameRate is calculated (correct me if it's not how it's calculated).

On my Windows 11 desktop with a 144Hz monitor (Vsync is enabled: glfwSwapInterval(1)), I get the expected result: all measurements give me approximately the same result of 144 FPS:

However, on my 60Hz MacBook Pro, I got a very unexpected result:

These results are weird for multiple reasons:

I really can't understand the results on my MacBook Pro, and I don't know if it's a bug with ImGui or if it should behave like this on a Mac for some reason. I don't think I made a mistake in the calculations. I calculated the average FPS with a circular buffer keeping the last 120 frames and using these formulas for a running average from Wikipedia, and I assume io.Framerate uses a similar calculation. If there are no mistakes or bugs and it is the intended behavior on macOS, then which of these values should I use to determine the delta time and the FPS: dt, io.DeltaTime or 1 / io.Framerate?

Here's the code used to make these measurements which is simply the glfw+opengl3 example with the calculations I made myself:

// Dear ImGui: standalone example application for GLFW + OpenGL 3, using programmable pipeline
// (GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.)

// Learn about Dear ImGui:
// - FAQ                  https://dearimgui.com/faq
// - Getting Started      https://dearimgui.com/getting-started
// - Documentation        https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp

#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#define GL_SILENCE_DEPRECATION
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <GLES2/gl2.h>
#endif
#include <GLFW/glfw3.h> // Will drag system OpenGL headers

// [Win32] Our example includes a copy of glfw3.lib pre-compiled with VS2010 to maximize ease of testing and compatibility with old VS compilers.
// To link with VS2010-era libraries, VS2015+ requires linking with legacy_stdio_definitions.lib, which we do using this pragma.
// Your own project should not be affected, as you are likely to link with a newer binary of GLFW that is adequate for your version of Visual Studio.
#if defined(_MSC_VER) && (_MSC_VER >= 1900) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS)
#pragma comment(lib, "legacy_stdio_definitions")
#endif

// This example can also compile and run with Emscripten! See 'Makefile.emscripten' for details.
#ifdef __EMSCRIPTEN__
#include "../libs/emscripten/emscripten_mainloop_stub.h"
#endif

static void glfw_error_callback(int error, const char* description)
{
    fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}

// Main code
int main(int, char**)
{
    glfwSetErrorCallback(glfw_error_callback);
    if (!glfwInit())
        return 1;

    // Decide GL+GLSL versions
#if defined(IMGUI_IMPL_OPENGL_ES2)
    // GL ES 2.0 + GLSL 100
    const char* glsl_version = "#version 100";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
    glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
#elif defined(__APPLE__)
    // GL 3.2 + GLSL 150
    const char* glsl_version = "#version 150";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 3.2+ only
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // Required on Mac
#else
    // GL 3.0 + GLSL 130
    const char* glsl_version = "#version 130";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
    //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 3.2+ only
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 3.0+ only
#endif

    // Create window with graphics context
    GLFWwindow* window = glfwCreateWindow(1280, 720, "Dear ImGui GLFW+OpenGL3 example", nullptr, nullptr);
    if (window == nullptr)
        return 1;
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1); // Enable vsync

    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;         // Enable Docking
    io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;       // Enable Multi-Viewport / Platform Windows
    //io.ConfigViewportsNoAutoMerge = true;
    //io.ConfigViewportsNoTaskBarIcon = true;

    // Setup Dear ImGui style
    ImGui::StyleColorsDark();
    //ImGui::StyleColorsLight();

    // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
    ImGuiStyle& style = ImGui::GetStyle();
    if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        style.WindowRounding = 0.0f;
        style.Colors[ImGuiCol_WindowBg].w = 1.0f;
    }

    // Setup Platform/Renderer backends
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);

    // Load Fonts
    // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them.
    // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple.
    // - If the file cannot be loaded, the function will return a nullptr. Please handle those errors in your application (e.g. use an assertion, or display an error and quit).
    // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call.
    // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use Freetype for higher quality font rendering.
    // - Read 'docs/FONTS.md' for more instructions and details.
    // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ !
    // - Our Emscripten build process allows embedding fonts to be accessible at runtime from the "fonts/" folder. See Makefile.emscripten for details.
    //io.Fonts->AddFontDefault();
    //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf", 18.0f);
    //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f);
    //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f);
    //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f);
    //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, nullptr, io.Fonts->GetGlyphRangesJapanese());
    //IM_ASSERT(font != nullptr);

    // Our state
    bool show_demo_window = true;
    bool show_another_window = false;
    ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);

    double dt = 0, fpsAvg = 0, fpsAvgGlfw = 0;
    constexpr int framesCount = 120;
    double recentFrames[framesCount];
    double recentFramesGlfw[framesCount];
    int index = 0, initialCount = 0;

    // Main loop
#ifdef __EMSCRIPTEN__
    // For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file.
    // You may manually call LoadIniSettingsFromMemory() to load settings from your own storage.
    io.IniFilename = nullptr;
    EMSCRIPTEN_MAINLOOP_BEGIN
#else
    while (!glfwWindowShouldClose(window))
#endif
    {
        const double frameStart = glfwGetTime();
        // Poll and handle events (inputs, window resize, etc.)
        // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
        // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
        // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
        // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
        glfwPollEvents();

        // Start the Dear ImGui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
        if (show_demo_window)
            ImGui::ShowDemoWindow(&show_demo_window);

        // 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
        {
            static float f = 0.0f;
            static int counter = 0;

            ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.

            ImGui::Text("This is some useful text.");               // Display some text (you can use a format strings too)
            ImGui::Checkbox("Demo Window", &show_demo_window);      // Edit bools storing our window open/close state
            ImGui::Checkbox("Another Window", &show_another_window);

            ImGui::SliderFloat("float", &f, 0.0f, 1.0f);            // Edit 1 float using a slider from 0.0f to 1.0f
            ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color

            if (ImGui::Button("Button"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
                counter++;
            ImGui::SameLine();
            ImGui::Text("counter = %d", counter);

            ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);

            ImGui::Separator();
            if (dt > 0)
            {
                ImGui::Text("ImGUI: %.3f ms, %.1f FPS", 1000.0 * io.DeltaTime, 1 / io.DeltaTime);
                ImGui::Text("GLFW: %.3f ms, %.1f FPS", 1000.0 * dt, 1 / dt);
                ImGui::Text("Average FPS (using io.DeltaTime): %.1f FPS", fpsAvg);
                ImGui::Text("Average FPS (using glfwGetTime()): %.1f FPS", fpsAvgGlfw);
            }

            ImGui::End();
        }

        // 3. Show another simple window.
        if (show_another_window)
        {
            ImGui::Begin("Another Window", &show_another_window);   // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
            ImGui::Text("Hello from another window!");
            if (ImGui::Button("Close Me"))
                show_another_window = false;
            ImGui::End();
        }

        // Rendering
        ImGui::Render();
        int display_w, display_h;
        glfwGetFramebufferSize(window, &display_w, &display_h);
        glViewport(0, 0, display_w, display_h);
        glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        // Update and Render additional Platform Windows
        // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
        //  For this specific demo app we could also call glfwMakeContextCurrent(window) directly)
        if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
        {
            GLFWwindow* backup_current_context = glfwGetCurrentContext();
            ImGui::UpdatePlatformWindows();
            ImGui::RenderPlatformWindowsDefault();
            glfwMakeContextCurrent(backup_current_context);
        }

        glfwSwapBuffers(window);

        const double newFps = 1 / io.DeltaTime;
        const double newFpsGlfw = (dt > 0) ? 1 / dt : newFps;
        if (initialCount < framesCount)
        {
            fpsAvg = (newFps + initialCount * fpsAvg) / (initialCount + 1);
            fpsAvgGlfw = (newFpsGlfw + initialCount * fpsAvgGlfw) / (initialCount + 1);
            ++initialCount;
        }
        else
        {
            fpsAvg += (newFps - recentFrames[index]) / framesCount;
            fpsAvgGlfw += (newFpsGlfw - recentFramesGlfw[index]) / framesCount;
        }
        recentFrames[index] = newFps;
        recentFramesGlfw[index] = newFpsGlfw;
        ++index;
        index %= framesCount;

        dt = glfwGetTime() - frameStart;
    }
#ifdef __EMSCRIPTEN__
    EMSCRIPTEN_MAINLOOP_END;
#endif

    // Cleanup
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}
ocornut commented 9 months ago

Hello Shai,

I sympathize with your issue and I see you've researched it well, but at heart this question isn't very much in the scope of dear imgui.
"I don't know if it's a bug with ImGui " it's not a bug in Dear ImGui since glfwSwapBuffers() is not a Dear ImGui function :)

ShaiAvr commented 9 months ago

@ocornut I did further testing according to your points, and here's what I found and some questions I still have:

Screenshot 2023-12-19 at 13 53 24

If the calculations are now correct and these results are normal for MacOs, I still don't understand what I should take for the time of the frame: dt, or 1 / io.Framerate? And what is the real FPS of the program, 120 or 300? I don't understand why the immediate FPS is always above 300 and the average is 120. It's very weird.

Here are the updated results:

Screenshot 2023-12-19 at 14 01 00

and the updated code:

// Dear ImGui: standalone example application for GLFW + OpenGL 3, using programmable pipeline
// (GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.)

// Learn about Dear ImGui:
// - FAQ                  https://dearimgui.com/faq
// - Getting Started      https://dearimgui.com/getting-started
// - Documentation        https://dearimgui.com/docs (same as your local docs/ folder).
// - Introduction, links and more at the top of imgui.cpp

#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#define GL_SILENCE_DEPRECATION
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <GLES2/gl2.h>
#endif
#include <GLFW/glfw3.h> // Will drag system OpenGL headers

#include <limits>

// [Win32] Our example includes a copy of glfw3.lib pre-compiled with VS2010 to maximize ease of testing and compatibility with old VS compilers.
// To link with VS2010-era libraries, VS2015+ requires linking with legacy_stdio_definitions.lib, which we do using this pragma.
// Your own project should not be affected, as you are likely to link with a newer binary of GLFW that is adequate for your version of Visual Studio.
#if defined(_MSC_VER) && (_MSC_VER >= 1900) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS)
#pragma comment(lib, "legacy_stdio_definitions")
#endif

// This example can also compile and run with Emscripten! See 'Makefile.emscripten' for details.
#ifdef __EMSCRIPTEN__
#include "../libs/emscripten/emscripten_mainloop_stub.h"
#endif

static void glfw_error_callback(int error, const char *description)
{
    fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}

// Main code
int main(int, char **)
{
    glfwSetErrorCallback(glfw_error_callback);
    if (!glfwInit())
        return 1;

        // Decide GL+GLSL versions
#if defined(IMGUI_IMPL_OPENGL_ES2)
    // GL ES 2.0 + GLSL 100
    const char *glsl_version = "#version 100";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
    glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
#elif defined(__APPLE__)
    // GL 3.2 + GLSL 150
    const char *glsl_version = "#version 150";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);           // Required on Mac
#else
    // GL 3.0 + GLSL 130
    const char *glsl_version = "#version 130";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
    // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 3.2+ only
    // glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 3.0+ only
#endif

    // Create window with graphics context
    GLFWwindow *window = glfwCreateWindow(1280, 720, "Dear ImGui GLFW+OpenGL3 example", nullptr, nullptr);
    if (window == nullptr)
        return 1;
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1); // Enable vsync

    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO &io = ImGui::GetIO();
    (void)io;
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;  // Enable Gamepad Controls
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;     // Enable Docking
    io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;   // Enable Multi-Viewport / Platform Windows
    // io.ConfigViewportsNoAutoMerge = true;
    // io.ConfigViewportsNoTaskBarIcon = true;

    // Setup Dear ImGui style
    ImGui::StyleColorsDark();
    // ImGui::StyleColorsLight();

    // When viewports are enabled we tweak WindowRounding/WindowBg so platform windows can look identical to regular ones.
    ImGuiStyle &style = ImGui::GetStyle();
    if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
    {
        style.WindowRounding = 0.0f;
        style.Colors[ImGuiCol_WindowBg].w = 1.0f;
    }

    // Setup Platform/Renderer backends
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);

    // Load Fonts
    // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them.
    // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple.
    // - If the file cannot be loaded, the function will return a nullptr. Please handle those errors in your application (e.g. use an assertion, or display an error and quit).
    // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call.
    // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use Freetype for higher quality font rendering.
    // - Read 'docs/FONTS.md' for more instructions and details.
    // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ !
    // - Our Emscripten build process allows embedding fonts to be accessible at runtime from the "fonts/" folder. See Makefile.emscripten for details.
    // io.Fonts->AddFontDefault();
    // io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf", 18.0f);
    // io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f);
    // io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f);
    // io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f);
    // ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, nullptr, io.Fonts->GetGlyphRangesJapanese());
    // IM_ASSERT(font != nullptr);

    // Our state
    bool show_demo_window = true;
    bool show_another_window = false;
    ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);

    double dt = 0, timeAvg = 0, timeAvgGlfw = 0;
    constexpr int framesCount = 120;
    double recentFrames[framesCount];
    double recentFramesGlfw[framesCount];
    const double inf = std::numeric_limits<double>::infinity();
    int index = 0, initialCount = 0;

    // Main loop
#ifdef __EMSCRIPTEN__
    // For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file.
    // You may manually call LoadIniSettingsFromMemory() to load settings from your own storage.
    io.IniFilename = nullptr;
    EMSCRIPTEN_MAINLOOP_BEGIN
#else
    while (!glfwWindowShouldClose(window))
#endif
    {
        const double frameStart = glfwGetTime();
        // Poll and handle events (inputs, window resize, etc.)
        // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
        // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
        // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
        // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
        glfwPollEvents();

        // Start the Dear ImGui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
        if (show_demo_window)
            ImGui::ShowDemoWindow(&show_demo_window);

        // 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
        {
            static float f = 0.0f;
            static int counter = 0;

            ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it.

            ImGui::Text("This is some useful text.");          // Display some text (you can use a format strings too)
            ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state
            ImGui::Checkbox("Another Window", &show_another_window);

            ImGui::SliderFloat("float", &f, 0.0f, 1.0f);             // Edit 1 float using a slider from 0.0f to 1.0f
            ImGui::ColorEdit3("clear color", (float *)&clear_color); // Edit 3 floats representing a color

            if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated)
                counter++;
            ImGui::SameLine();
            ImGui::Text("counter = %d", counter);

            ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);

            ImGui::Separator();
            ImGui::Text(
                "ImGUI: %.3f ms, %.1f FPS",
                1000.0 * io.DeltaTime,
                io.DeltaTime > 0.0 ? 1 / io.DeltaTime : inf);
            ImGui::Text("GLFW: %.3f ms, %.1f FPS", 1000.0 * dt, dt > 0.0 ? 1 / dt : inf);
            ImGui::Text("Average FPS (using io.DeltaTime): %.1f FPS", timeAvg > 0.0 ? 1 / timeAvg : inf);
            ImGui::Text("Average FPS (using glfwGetTime()): %.1f FPS", timeAvgGlfw > 0.0 ? 1 / timeAvgGlfw : inf);

            ImGui::End();
        }

        // 3. Show another simple window.
        if (show_another_window)
        {
            ImGui::Begin("Another Window", &show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
            ImGui::Text("Hello from another window!");
            if (ImGui::Button("Close Me"))
                show_another_window = false;
            ImGui::End();
        }

        // Rendering
        ImGui::Render();
        int display_w, display_h;
        glfwGetFramebufferSize(window, &display_w, &display_h);
        glViewport(0, 0, display_w, display_h);
        glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        // Update and Render additional Platform Windows
        // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
        //  For this specific demo app we could also call glfwMakeContextCurrent(window) directly)
        if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
        {
            GLFWwindow *backup_current_context = glfwGetCurrentContext();
            ImGui::UpdatePlatformWindows();
            ImGui::RenderPlatformWindowsDefault();
            glfwMakeContextCurrent(backup_current_context);
        }

        glfwSwapBuffers(window);

        const double newDt = io.DeltaTime;
        if (initialCount < framesCount)
        {
            timeAvg = (newDt + initialCount * timeAvg) / (initialCount + 1);
            timeAvgGlfw = (dt + initialCount * timeAvgGlfw) / (initialCount + 1);
            ++initialCount;
        }
        else
        {
            timeAvg += (newDt - recentFrames[index]) / framesCount;
            timeAvgGlfw += (dt - recentFramesGlfw[index]) / framesCount;
        }
        recentFrames[index] = newDt;
        recentFramesGlfw[index] = dt;
        ++index;
        index %= framesCount;

        dt = glfwGetTime() - frameStart;
    }
#ifdef __EMSCRIPTEN__
    EMSCRIPTEN_MAINLOOP_END;
#endif

    // Cleanup
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}
GamingMinds-DanielC commented 9 months ago

Your math is way off. You can't just average frame rates to get an average frame rate. Simple example:

You have frame a taking 0.8s (= 1.25 fps) and frame b taking 0.2s (= 5 fps). Frame a + b together take 0.8s + 0.2s = 1s, so that's 2 frames in the span of 1s meaning 2 fps. Self evident with this example data. What would your averaging yield? (1.25 fps + 5 fps) / 2 = 3.125 fps. Reciprocals (that's what frame rates basically are, the reciprocal of frame times) don't average like that.

If you want correct results, you need to divide the total number of frames measured by the total time measured.

Edit: to clarify, I'm talking about your math in your initial post, your updated code just dropped while I was writing this.

Edit 2: your updated math is a lot better, but still not good. You are accumulating rounding errors if you just keep adjusting an average time like that. I suggest to measure only once per frame and memorize the exact timestamps (not framerates or deltas). Then you can calculate an exact time difference for your n frames and from that the average framerate over those n frames.

ShaiAvr commented 9 months ago

@GamingMinds-DanielC Yes, my updated code gives an average of 120 FPS like io.Framerate. It still doesn't explain why the immediate FPS (1 / dt or 1 / io.DeltaTime) is still above 300 FPS which is almost 3 times the average.

ocornut commented 9 months ago

About the point you made about glfwGetTime() - the same argument holds for any function that returns the current time. How else am I supposed to measure the time of a frame?

Compare current time with actual previous time value (as our backend do). Anything else will cause a small difference. I don't think that's the main cause of your issue here, but for correctness you should switch to that.

ocornut commented 9 months ago

Your code is constantly using the difference between two very small values, divided to be made even smaller, added to an accumulator. Without understanding it fully it seems like a recipe for drift or inaccuracies (assuming it's even correct :I haven't thought about it deeply enough to tell).

Compare to the code we use:

g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; // Accumulate difference
g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; // Store new value
g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); // Current looping index
g.FramerateSecPerFrameCount = ImMin(g.FramerateSecPerFrameCount + 1, IM_ARRAYSIZE(g.FramerateSecPerFrame)); // Max at 120
g.IO.Framerate = (g.FramerateSecPerFrameAccum > 0.0f) ? (1.0f / (g.FramerateSecPerFrameAccum / (float)g.FramerateSecPerFrameCount)) : FLT_MAX; // Divide on output

Which doesn't overly accumulate errors.

ShaiAvr commented 9 months ago

@ocornut Your accumulating code should be equivalent to mine. The only difference is that you are accumulating the frame times and then divide by the frames count, while I accumulate the frames time divided by the frames count, which means that on output I only need to calculate 1 divided by my accumulator. Mathematically, it should be equivalent, and as you can see in the updated results, the average FPS I calculated is approximately the same as io.Framerate. I'll probably adapt my code to work like yours because it does seem clearer to me than my initial solution, but they both work. My problem is that the immediate FPS which doesn't involve any averaging and is simply 1 / dt or 1 / io.DeltaTime is consistently higher than the average FPS which seems odd.

Compare current time with actual previous time value (as our backend do). Anything else will cause a small difference. I don't think that's the main cause of your issue here, but for correctness you should switch to that.

I am not sure I understand what you mean. Do you mean that I should call glfwGetTime() only once per frame? Something like:

double prevTime = glfwGetTime(), dt = 0.0;
while (!glfwWindowShouldClose(window))
{
    // Rendering and swapping buffers
    // At the end of the frame:
    double t = glfwGetTime();
    dt = t - prevTime;
    prevTime = t;
}
ocornut commented 9 months ago

while I accumulate the frames time divided by the frames count,

You are accumulating the frame time difference divided by the frame count? "timeAvg += (newDt - recentFrames[index]) / framesCount;"

Do you mean that I should call glfwGetTime() only once per frame? Something like:

Yes.

The bottom line is we understand you have an issue but you should have understood since the first reply the issue has nothing to do with Dear ImGui and at this point are you using Dear ImGui resources to debug your code, which I'm sure you understand is not ideal.

ShaiAvr commented 9 months ago

You are accumulating the frame time difference divided by the frame count?

Yes.

The bottom line is we understand you have an issue but you should have understood since the first reply the issue has nothing to do with Dear ImGui and at this point are you using Dear ImGui resources to debug your code, which I'm sure you understand is not ideal.

At the beginning, I believed it could probably be related to Dear ImGui, but I can agree it probably isn’t the root of the problem. I find it odd that on my Windows desktop, I got consistent FPS that matches the screen’s refresh rate, even with the wrong calculations, while on my MacBook Pro, both the immediate and average FPS (calculated correctly, of course) is higher than the refresh rate, and the average FPS is consistently lower than the immediate FPS.

If you have any idea what causes this behavior on the Mac and if it’s normal, I’d like to know. Regardless of that, you certainly helped me improve my code and my understanding of how it works.

ocornut commented 9 months ago

I would suggest to add something is:

if (ImGui::IsKeyPressed(ImGuiKey_F))
{
   printf() last 10 values
}

And see what happens with your immediate dt.

ShaiAvr commented 9 months ago

@ocornut I did that and found out that the time deltas fluctuate between around 2ms to 14ms which is between 70-400 immediate FPS approximately. Here's a sample data:

io.DeltaTime: 1.705 ms (586.6 FPS)
glfwGetTime(): 14.935 ms (67.0 FPS)
io.DeltaTime: 14.401 ms (69.4 FPS)
glfwGetTime(): 1.729 ms (578.3 FPS)
io.DeltaTime: 2.576 ms (388.2 FPS)
glfwGetTime(): 14.383 ms (69.5 FPS)
io.DeltaTime: 13.832 ms (72.3 FPS)
glfwGetTime(): 2.576 ms (388.1 FPS)
io.DeltaTime: 2.675 ms (373.8 FPS)
glfwGetTime(): 13.828 ms (72.3 FPS)
io.DeltaTime: 13.558 ms (73.8 FPS)
glfwGetTime(): 2.701 ms (370.2 FPS)
io.DeltaTime: 2.979 ms (335.7 FPS)
glfwGetTime(): 13.532 ms (73.9 FPS)
io.DeltaTime: 13.738 ms (72.8 FPS)
glfwGetTime(): 3.012 ms (332.0 FPS)
io.DeltaTime: 2.942 ms (339.9 FPS)
glfwGetTime(): 13.705 ms (73.0 FPS)
io.DeltaTime: 13.850 ms (72.2 FPS)
glfwGetTime(): 2.976 ms (336.1 FPS)

That certainly makes the average seem more logical now. The only question that remains is why the time differences between frames fluctuate on my MacBook Pro so much. For comparison, on my Windows desktop, I get roughly constant 7 ms which corresponds to 144 immediate FPS as expected:

io.DeltaTime: 7.829 ms (127.7 FPS)
glfwGetTime(): 6.539 ms (152.9 FPS)
io.DeltaTime: 7.035 ms (142.2 FPS)
glfwGetTime(): 7.462 ms (134.0 FPS)
io.DeltaTime: 6.722 ms (148.8 FPS)
glfwGetTime(): 7.034 ms (142.2 FPS)
io.DeltaTime: 6.931 ms (144.3 FPS)
glfwGetTime(): 6.782 ms (147.5 FPS)
io.DeltaTime: 6.821 ms (146.6 FPS)
glfwGetTime(): 6.873 ms (145.5 FPS)
io.DeltaTime: 6.412 ms (156.0 FPS)
glfwGetTime(): 6.823 ms (146.6 FPS)
io.DeltaTime: 7.765 ms (128.8 FPS)
glfwGetTime(): 6.414 ms (155.9 FPS)
io.DeltaTime: 8.617 ms (116.0 FPS)
glfwGetTime(): 7.770 ms (128.7 FPS)
io.DeltaTime: 8.663 ms (115.4 FPS)
glfwGetTime(): 8.751 ms (114.3 FPS)
io.DeltaTime: 6.195 ms (161.4 FPS)
glfwGetTime(): 8.550 ms (117.0 FPS)
ocornut commented 9 months ago

The only question that remains is why the time differences between frames fluctuate on my MacBook Pro so much.

But none of it is related to Dear ImGui at this point and if you focus your research on not considering Dear ImGui you'll more likely find a good answer elsewhere. I would suggest to try as many backends at possible (GLFW, SDL, raw OSX) so see if this has any impact.

(It's difficult to read the log but there's no reason to be measuring dt in two different ways as the value won't be exactly equal.)