#include "frame_pacer.hpp" #include #include #include #include #include #include "log.hpp" namespace t5de::frame_pacer { namespace { // The client's own rate: its ticks already draw these frames. constexpr DWORD clientRate = 30; // Extra frames only if a frame takes less than this share of the interval. constexpr double affordable = 0.75; // Uncapped, the client is left idle this share of a frame's time between frames. constexpr double uncappedIdle = 1.0 / 3.0; // The user's frame rate, from the registry (unset or unreadable: the client's own). DWORD read_rate() { DWORD rate = 0; DWORD size = sizeof(rate); if (RegGetValueW(HKEY_CURRENT_USER, L"Software\\T5DE\\Graphics", L"FrameRate", RRF_RT_REG_DWORD, nullptr, &rate, &size) != ERROR_SUCCESS) { return clientRate; } return rate; } double now_seconds() { static const double frequency = [] { LARGE_INTEGER value; QueryPerformanceFrequency(&value); return static_cast(value.QuadPart); }(); LARGE_INTEGER counter; QueryPerformanceCounter(&counter); return static_cast(counter.QuadPart) / frequency; } class Pacer { public: static Pacer& get() { static Pacer pacer; return pacer; } void add(HWND window) { const std::scoped_lock lock(mutex_); windows_.push_back({.window = window}); if (!started_) { started_ = true; std::thread([this] { run(); }).detach(); // runs while the client does } } void remove(HWND window) { const std::scoped_lock lock(mutex_); std::erase_if(windows_, [window](const Window& w) { return w.window == window; }); } void frameDrawn(HWND window, double seconds) { const std::scoped_lock lock(mutex_); for (Window& w : windows_) { if (w.window == window) { w.pending = false; w.frameTime = w.frameTime == 0 ? seconds : w.frameTime * 0.9 + seconds * 0.1; w.drawnAt = now_seconds(); } } } private: struct Window { HWND window; bool pending = false; // a repaint asked for and not yet drawn double frameTime = 0; // seconds a frame takes the client, averaged double drawnAt = 0; // when its last frame was drawn double askedAt = 0; // when a repaint was last asked for }; // Re-reads the setting now and then; above the client's rate, asks each window for a repaint at the rate, when // affordable. void run() { HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); double checked = 0; DWORD rate = clientRate; for (;;) { const double now = now_seconds(); if (now - checked >= 0.5) { checked = now; const DWORD read = read_rate(); if (read != rate) { rate = read; log::info("frame pacer: frame rate {}", rate == 0 ? std::string("uncapped") : std::to_string(rate)); } } if (rate != 0 && rate <= clientRate) { Sleep(100); continue; } const double interval = rate == 0 ? 0 : 1.0 / rate; { const std::scoped_lock lock(mutex_); for (Window& w : windows_) { if (w.pending && now - w.askedAt < 0.25) { continue; // not drawn yet (asked again after a while: the client may have skipped it) } const bool due = rate == 0 ? now - w.drawnAt >= w.frameTime * uncappedIdle : now - w.askedAt >= interval && w.frameTime < interval * affordable; if (due) { w.pending = true; w.askedAt = now; InvalidateRect(w.window, nullptr, FALSE); } } } // Wake often enough for the rate (1 ms at most when uncapped). const double wait = std::clamp(interval / 4, 0.0005, 0.004); LARGE_INTEGER due{.QuadPart = -static_cast(wait * 10'000'000)}; // relative, 100 ns units if (timer != nullptr && SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) { WaitForSingleObject(timer, INFINITE); } else { Sleep(1); } } } std::mutex mutex_; std::vector windows_; bool started_ = false; }; } // namespace void add(HWND window) { Pacer::get().add(window); } void remove(HWND window) { Pacer::get().remove(window); } void frameDrawn(HWND window, double seconds) { Pacer::get().frameDrawn(window, seconds); } } // namespace t5de::frame_pacer