CI / build (push) Successful in 1m31s
The frame pacer repaints the client's 3D windows at the user's frame rate (HKCU\Software\T5DE\Graphics\FrameRate: 30, the default, adds nothing; 60, 120, 240, 360; 0 uncapped), so the client draws frames between its 30-per-second steps. Only when affordable: a window is asked once its last frame is drawn, and only if its frames take under three quarters of the interval; uncapped, the client is left idle a third of a frame between frames. The engine's stats now log frames per second. The extra frames show the same scene for now. Co-Authored-By: Claude Opus 5.5 <[email protected]>
153 lines
4.9 KiB
C++
153 lines
4.9 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;
|
|
// 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<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 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<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
|