CI / build (push) Successful in 32s
The pacer counted only its own frames, so the client's 30 came on top (60 gave about 80), and it added no frames at all when the rate was out of reach. It now asks for a window's next frame once the interval since its last frame began has passed, and the client has had a third of a frame's time idle: all frames within the rate, and as many as the client can draw when it can't reach it. The engine's stats also break a frame's time down: sending to the renderer process, bytes sent, buffer updates. Co-Authored-By: Claude Opus 5.5 <[email protected]>
150 lines
4.8 KiB
C++
150 lines
4.8 KiB
C++
#include "frame_pacer.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "log.hpp"
|
|
|
|
namespace t5de::frame_pacer {
|
|
|
|
namespace {
|
|
|
|
// The client's own rate: its ticks already draw these frames.
|
|
constexpr DWORD clientRate = 30;
|
|
// The client is left idle at least this share of a frame's time between frames.
|
|
constexpr double idleShare = 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<double>(value.QuadPart);
|
|
}();
|
|
LARGE_INTEGER counter;
|
|
QueryPerformanceCounter(&counter);
|
|
return static_cast<double>(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 repaints at the rate, or as often
|
|
// as leaves the client its idle time.
|
|
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)
|
|
}
|
|
// Since the last frame ended: the rest of the interval since it began, and the idle time.
|
|
if (now - w.drawnAt >= std::max(interval - w.frameTime, w.frameTime * idleShare)) {
|
|
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<LONGLONG>(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<Window> 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
|