Fix inconsistencies with the FPS scheduler (#241)

The current Fps manager will sometime skip the intended delay and start
the next frame immediately, I'm not 100% sure why it happens, but I
rewrote most of it to stay consistent and improved the overall precision
by compensating for OS wake delay after sleep and finishing with a busy
wait
I also changed the behavior of mlx_set_fps_goal to allow zero/negative
values and treat them as uncapped (like most graphics libraries)
This commit is contained in:
Daemo
2026-08-08 15:45:56 +02:00
committed by GitHub
parent c9df651a14
commit 63fe3088bf
4 changed files with 43 additions and 34 deletions
+12 -8
View File
@@ -1,26 +1,30 @@
#ifndef __MLX_FPS__ #ifndef __MLX_FPS__
#define __MLX_FPS__ #define __MLX_FPS__
#include <chrono>
namespace mlx namespace mlx
{ {
typedef std::chrono::steady_clock fps_clock;
class FpsManager class FpsManager
{ {
public: public:
FpsManager() = default; FpsManager() = default;
void Init(); void Init();
bool Update(); void WaitUntilNextFrame();
inline void SetMaxFPS(std::uint32_t fps) noexcept { m_max_fps = fps; m_ns = 1000000000.0 / fps; } inline void SetMaxFPS(std::uint32_t fps) noexcept { m_target_delta = fps_clock::duration(std::chrono::seconds(1)) / fps;}
~FpsManager() = default; ~FpsManager() = default;
private: private:
double m_ns = 1000000000.0 / 1'337'000.0; fps_clock::time_point m_current_time;
std::int64_t m_fps_before = 0; fps_clock::time_point m_target_time;
std::int64_t m_fps_now = 0; fps_clock::time_point m_last_time_record;
std::int64_t m_timer = 0; fps_clock::duration m_delta_time = fps_clock::duration().zero();
std::uint32_t m_max_fps = 1'337'000; fps_clock::duration m_target_delta = fps_clock::duration().zero();
std::uint32_t m_fps_elapsed_time = 0; fps_clock::duration m_sleep_margin = std::chrono::microseconds(200);
}; };
} }
+2 -3
View File
@@ -8,7 +8,7 @@
namespace mlx namespace mlx
{ {
Application::Application() : p_mem_manager(std::make_unique<MemManager>()), p_sdl_manager(std::make_unique<SDLManager>()), m_fps(), m_in() Application::Application() : p_mem_manager(std::make_unique<MemManager>()), p_sdl_manager(std::make_unique<SDLManager>()), m_fps(), m_in()
{ {
MLX_PROFILE_FUNCTION(); MLX_PROFILE_FUNCTION();
std::srand(std::time(nullptr)); std::srand(std::time(nullptr));
@@ -33,8 +33,7 @@ namespace mlx
while(m_in.IsRunning()) while(m_in.IsRunning())
{ {
if(!m_fps.Update()) m_fps.WaitUntilNextFrame();
continue;
m_in.FetchInputs(); m_in.FetchInputs();
+6 -7
View File
@@ -45,10 +45,9 @@ extern "C"
void mlx_set_fps_goal(mlx_context mlx, int fps) void mlx_set_fps_goal(mlx_context mlx, int fps)
{ {
MLX_CHECK_APPLICATION_POINTER(mlx); MLX_CHECK_APPLICATION_POINTER(mlx);
if(fps < 0) if(fps <= 0)
mlx::Error("You cannot set a negative FPS cap (nice try)"); fps = -1;
else mlx->app->SetFPSCap(static_cast<std::uint32_t>(fps));
mlx->app->SetFPSCap(static_cast<std::uint32_t>(fps));
} }
void mlx_destroy_context(mlx_context mlx) void mlx_destroy_context(mlx_context mlx)
@@ -313,14 +312,14 @@ extern "C"
mlx::Error("Font loader: filepath is NULL"); mlx::Error("Font loader: filepath is NULL");
return; return;
} }
std::filesystem::path file(filepath); std::filesystem::path file(filepath);
if (std::strcmp(filepath, "default") != 0 && !std::filesystem::exists(file)) if (std::strcmp(filepath, "default") != 0 && !std::filesystem::exists(file))
{ {
mlx::Error("TTF loader: unable to find file '%'", filepath); mlx::Error("TTF loader: unable to find file '%'", filepath);
return; return;
} }
if(std::strcmp(filepath, "default") != 0) if(std::strcmp(filepath, "default") != 0)
{ {
if(file.extension() != ".ttf" && file.extension() != ".tte") if(file.extension() != ".ttf" && file.extension() != ".tte")
@@ -350,7 +349,7 @@ extern "C"
mlx::Error("Font loader: filepath is NULL"); mlx::Error("Font loader: filepath is NULL");
return; return;
} }
std::filesystem::path file(filepath); std::filesystem::path file(filepath);
if (std::strcmp(filepath, "default") != 0 && !std::filesystem::exists(file)) if (std::strcmp(filepath, "default") != 0 && !std::filesystem::exists(file))
{ {
+23 -16
View File
@@ -1,30 +1,37 @@
#include <PreCompiled.h> #include <PreCompiled.h>
#include <Core/Fps.h> #include <Core/Fps.h>
#ifndef __APPLE__
#include <emmintrin.h>
#endif
namespace mlx namespace mlx
{ {
void FpsManager::Init() void FpsManager::Init()
{ {
m_timer = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count()); m_current_time = fps_clock::now();
m_fps_before = m_timer; m_target_time = m_current_time + m_target_delta;
m_fps_now = m_timer;
} }
bool FpsManager::Update() void FpsManager::WaitUntilNextFrame()
{ {
using namespace std::chrono_literals; m_current_time = fps_clock::now();
m_fps_now = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count()); if(m_current_time < m_target_time)
if(std::chrono::duration<std::uint64_t>{m_fps_now - m_timer} >= 1s)
m_timer += m_fps_now;
m_fps_elapsed_time = m_fps_now - m_fps_before;
if(m_fps_elapsed_time >= m_ns)
{ {
m_fps_before += m_ns; std::this_thread::sleep_until(m_target_time - m_sleep_margin);
return true; m_current_time = fps_clock::now();
while (m_current_time < m_target_time)
{
#ifndef __APPLE__
_mm_pause(); // reduces CPU usage on x86 without yielding
#endif
m_current_time = fps_clock::now();
}
} }
std::this_thread::sleep_for(std::chrono::duration<double, std::nano>(m_ns - 1)); else if (m_target_time < m_current_time - m_target_delta * 4)
return false; m_target_time = m_current_time;
m_target_time += m_target_delta;
m_delta_time = m_current_time - m_last_time_record;
m_last_time_record = m_current_time;
} }
} }