impr: Much more accurate frame rate limiting

Many thanks to @ws909 in https://github.com/glfw/glfw/issues/2737
This commit is contained in:
WerWolv
2025-07-11 13:29:56 +02:00
parent 82c318f91d
commit 8c59684c14
2 changed files with 98 additions and 120 deletions

View File

@@ -56,9 +56,7 @@ namespace hex {
void drawImGui();
void drawWithShader();
void unlockFrameRate();
void forceNewFrame();
GLFWwindow *m_window = nullptr;
ImGuiTestEngine *m_testEngine = nullptr;
@@ -76,17 +74,9 @@ namespace hex {
u32 m_searchBarPosition = 0;
bool m_emergencyPopupOpen = false;
std::jthread m_frameRateThread;
std::chrono::duration<double, std::nano> m_remainingUnlockedTime;
std::mutex m_sleepMutex;
std::atomic<bool> m_sleepFlag;
std::condition_variable m_sleepCondVar;
std::mutex m_wakeupMutex;
std::atomic<bool> m_wakeupFlag;
std::condition_variable m_wakeupCondVar;
bool m_shouldUnlockFrameRate = false;
double m_fpsUnlockedEndTime = 0.0;
bool m_waitEventsBlocked = false;
gl::Shader m_postProcessingShader;
};

View File

@@ -2,6 +2,8 @@
#include <hex.hpp>
#include <windows.h>
#include <hex/api/plugin_manager.hpp>
#include <hex/api/content_registry.hpp>
#include <hex/api/imhex_api.hpp>
@@ -105,9 +107,6 @@ namespace hex {
}
Window::~Window() {
m_frameRateThread.request_stop();
m_frameRateThread.join();
EventProviderDeleted::unsubscribe(this);
RequestCloseImHex::unsubscribe(this);
RequestUpdateWindowTitle::unsubscribe(this);
@@ -227,6 +226,12 @@ namespace hex {
log::error("{}", message);
}
void Window::unlockFrameRate() {
glfwPostEmptyEvent();
m_shouldUnlockFrameRate = true;
}
void Window::fullFrame() {
[[maybe_unused]] static u32 crashWatchdog = 0;
@@ -267,8 +272,43 @@ namespace hex {
void Window::loop() {
glfwShowWindow(m_window);
double returnToIdleTime = 0;
constexpr static auto IdleFPS = 5.0;
constexpr static auto FrameRateUnlockDuration = 1;
double idleFrameTime = 1.0 / IdleFPS;
double targetFrameTime = idleFrameTime;
double longestExceededFrameTime = 0.0;
while (!glfwWindowShouldClose(m_window)) {
m_lastStartFrameTime = glfwGetTime();
const auto maxFPS = ImHexApi::System::getTargetFPS();
auto maxFrameTime = [&]() {
if (maxFPS < 15) {
// Use the monitor's refresh rate
auto monitor = glfwGetPrimaryMonitor();
if (monitor != nullptr) {
auto videoMode = glfwGetVideoMode(monitor);
if (videoMode != nullptr) {
return 1.0 / videoMode->refreshRate;
}
}
// Fallback to 60 FPS if real monitor refresh rate cannot be determined
return 1.0 / 60.0;
} else if (maxFPS > 200) {
// Don't limit the frame rate at all
return 0.0;
} else {
// Do regular frame rate limiting
return 1.0 / maxFPS;
}
}();
auto frameTimeStart = glfwGetTime();
glfwPollEvents();
{
int x = 0, y = 0;
@@ -285,8 +325,6 @@ namespace hex {
glfwWaitEvents();
}
m_lastStartFrameTime = glfwGetTime();
static ImVec2 lastWindowSize = ImHexApi::System::getMainWindowSize();
if (ImHexApi::System::impl::isWindowResizable()) {
glfwSetWindowSizeLimits(m_window, 480_scaled, 360_scaled, GLFW_DONT_CARE, GLFW_DONT_CARE);
@@ -297,29 +335,9 @@ namespace hex {
this->fullFrame();
ImHexApi::System::impl::setLastFrameTime(glfwGetTime() - m_lastStartFrameTime);
{
while (true) {
glfwPollEvents();
if (ImHexApi::System::getTargetFPS() >= 200)
break;
{
std::unique_lock lock(m_sleepMutex);
m_sleepCondVar.wait(lock);
if (m_sleepFlag.exchange(false))
break;
}
}
}
m_lastFrameTime = glfwGetTime() - m_lastStartFrameTime;
// Unlock frame rate if any mouse button is being held down to allow drag scrolling to be smooth
if (ImGui::IsAnyMouseDown())
this->unlockFrameRate();
unlockFrameRate();
// Unlock frame rate if any modifier key is held down since they don't generate key repeat events
if (
@@ -328,17 +346,53 @@ namespace hex {
ImGui::IsKeyPressed(ImGuiKey_LeftSuper) || ImGui::IsKeyPressed(ImGuiKey_RightSuper) ||
ImGui::IsKeyPressed(ImGuiKey_LeftAlt) || ImGui::IsKeyPressed(ImGuiKey_RightAlt)
) {
this->unlockFrameRate();
unlockFrameRate();
}
// Unlock frame rate if there's more than one viewport since these don't call the glfw callbacks registered here
if (ImGui::GetPlatformIO().Viewports.size() > 1)
this->unlockFrameRate();
unlockFrameRate();
// Unlock frame rate if there's any task running that shows a loading animation
if (TaskManager::getRunningTaskCount() > 0 || TaskManager::getRunningBlockingTaskCount() > 0) {
this->unlockFrameRate();
glfwPostEmptyEvent();
unlockFrameRate();
}
auto frameTime = glfwGetTime() - frameTimeStart;
if (glfwGetTime() > returnToIdleTime) {
targetFrameTime = idleFrameTime;
}
while (frameTime < targetFrameTime - longestExceededFrameTime) {
auto remainingFrameTime = targetFrameTime - frameTime;
glfwWaitEventsTimeout(remainingFrameTime);
auto newFrameTime = glfwGetTime() - frameTimeStart;
auto elapsedWaitTime = newFrameTime - frameTime;
// Returned early; did not time out.
if (elapsedWaitTime < remainingFrameTime && glfwGetTime() > returnToIdleTime && m_shouldUnlockFrameRate) {
returnToIdleTime = glfwGetTime() + FrameRateUnlockDuration;
targetFrameTime = maxFrameTime;
}
m_shouldUnlockFrameRate = false;
frameTime = newFrameTime;
}
auto exceedTime = frameTime - targetFrameTime;
if (!m_waitEventsBlocked)
longestExceededFrameTime = std::max(exceedTime, longestExceededFrameTime);
m_waitEventsBlocked = false;
while (frameTime < maxFrameTime) {
frameTime = glfwGetTime() - frameTimeStart;
}
ImHexApi::System::impl::setLastFrameTime(glfwGetTime() - frameTimeStart);
}
// Hide the window as soon as the render loop exits to make the window
@@ -918,23 +972,6 @@ namespace hex {
#endif
}
void Window::unlockFrameRate() {
{
std::scoped_lock lock(m_wakeupMutex);
m_remainingUnlockedTime = std::chrono::seconds(2LL);
}
this->forceNewFrame();
}
void Window::forceNewFrame() {
std::scoped_lock lock(m_wakeupMutex);
m_wakeupFlag = true;
m_wakeupCondVar.notify_all();
}
void Window::initGLFW() {
auto initialWindowProperties = ImHexApi::System::getInitialWindowProperties();
glfwSetErrorCallback([](int error, const char *desc) {
@@ -1047,6 +1084,14 @@ namespace hex {
win->unlockFrameRate();
};
static const auto markWaitEventsBlocked = [](GLFWwindow *, auto ...) {
auto win = static_cast<Window *>(glfwGetWindowUserPointer(ImHexApi::System::getMainWindowHandle()));
if (win == nullptr)
return;
win->m_waitEventsBlocked = true;
};
static const auto isMainWindow = [](GLFWwindow *window) {
return window == ImHexApi::System::getMainWindowHandle();
};
@@ -1054,6 +1099,7 @@ namespace hex {
// Register window move callback
glfwSetWindowPosCallback(m_window, [](GLFWwindow *window, int x, int y) {
unlockFrameRate(window);
markWaitEventsBlocked(window);
if (!isMainWindow(window)) return;
@@ -1063,11 +1109,13 @@ namespace hex {
glfwGetWindowSize(window, &width, &height);
ImHexApi::System::impl::setMainWindowPosition(x, y);
ImHexApi::System::impl::setMainWindowSize(width, height);
});
// Register window resize callback
glfwSetWindowSizeCallback(m_window, [](GLFWwindow *window, [[maybe_unused]] int width, [[maybe_unused]] int height) {
unlockFrameRate(window);
markWaitEventsBlocked(window);
if (!isMainWindow(window)) return;
@@ -1174,66 +1222,6 @@ namespace hex {
});
glfwSetWindowSizeLimits(m_window, 480_scaled, 360_scaled, GLFW_DONT_CARE, GLFW_DONT_CARE);
m_frameRateThread = std::jthread([this](const std::stop_token &stopToken) {
using Duration = std::chrono::duration<double, std::nano>;
Duration passedTime = {};
std::chrono::steady_clock::time_point startTime = {}, endTime = {};
Duration requestedFrameTime = {};
float targetFps = 0;
const auto nativeFps = []() -> float {
if (const auto monitor = glfwGetPrimaryMonitor(); monitor != nullptr) {
if (const auto videoMode = glfwGetVideoMode(monitor); videoMode != nullptr) {
return videoMode->refreshRate;
}
}
return 60;
}();
while (!stopToken.stop_requested()) {
const auto iterationTime = endTime - startTime;
startTime = std::chrono::steady_clock::now();
targetFps = ImHexApi::System::getTargetFPS();
// If the target frame rate is below 15, use the current monitor's refresh rate
if (targetFps < 15) {
targetFps = nativeFps;
}
passedTime += iterationTime;
{
std::scoped_lock lock(m_sleepMutex);
if (m_remainingUnlockedTime > std::chrono::nanoseconds(0LL)) {
m_remainingUnlockedTime -= iterationTime;
} else {
targetFps = 5;
}
requestedFrameTime = (Duration(1.0E9) / targetFps) / 1.3;
if (passedTime >= requestedFrameTime) {
m_sleepFlag = true;
m_sleepCondVar.notify_all();
passedTime = {};
}
}
{
std::unique_lock lock(m_wakeupMutex);
m_wakeupCondVar.wait_for(lock, requestedFrameTime, [&] {
return m_wakeupFlag || stopToken.stop_requested();
});
m_wakeupFlag = false;
}
endTime = std::chrono::steady_clock::now();
}
});
}
void Window::resize(i32 width, i32 height) {