Closed ShaiAvr closed 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 :)
glfwGetTime()
is wise: what goes in-between is not accounted.@ocornut I did further testing according to your points, and here's what I found and some questions I still have:
dt
were correct and the FPS is indeed around ~300, and the measurement of 120 FPS from io.Framerate
is wrong.CMakeLists.txt
that is included with the example (I don't have experience with XCode and there are no XCode project files in the example like there are for Visual Studio, so I didn't want to try to create an XCode project). running cmake
generated an error that it couldn't find Vulkan because Vulkan_LIBRARY
and Vulkan_INCLUDE_DIR
were missing. I set these variables to the correct paths of the Vulkan libraries and headers and still got the same error:io.Framerate
. The immediate frame rate is still calculated with 1 / dt
or 1 / io.DeltaTime
and is still around 300 FPS which is odd. How can the immediate FPS be consistently 3 times higher than the average FPS?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?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:
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;
}
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.
@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.
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.
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.
@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;
}
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.
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.
I would suggest to add something is:
if (ImGui::IsKeyPressed(ImGuiKey_F))
{
printf() last 10 values
}
And see what happens with your immediate dt.
@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)
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.)
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 as1 / dt
. I also did the calculation using1 / io.DeltaTime
and by averaging the FPS over the last 120 frames likeio.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:
io.Framerate
and1 / io.Framerate
) show a consistent result of 120 FPS which is twice as much as the screen's refresh rate.1 / dt
or1 / io.DeltaTime
has some fluctuations but seems to be between 300 FPS and 400 FPS, which is much higher than the refresh rate.io.Framerate
gives about ~225 FPS which is almost twice as much asio.Framerate
and still doesn't make sense given the screen's refresh rate.These results are weird for multiple reasons:
io.Framerate
.io.Framerate
.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
or1 / io.Framerate
?Here's the code used to make these measurements which is simply the glfw+opengl3 example with the calculations I made myself: