Pace all frames to the frame rate, at best effort
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]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-28 21:16:22 -07:00
1 parent 05a7be5d41
commit 399d934a08
6 files changed
+45 -18

No files matched your search

+14 -6
View File
@@ -303,8 +303,10 @@ void Engine::endRendering()
LARGE_INTEGER end, frequency; LARGE_INTEGER end, frequency;
QueryPerformanceCounter(&end); QueryPerformanceCounter(&end);
QueryPerformanceFrequency(&frequency); QueryPerformanceFrequency(&frequency);
frame_pacer::frameDrawn(window_, static_cast<double>(end.QuadPart - frameStart_.QuadPart) / const double seconds =
static_cast<double>(frequency.QuadPart)); static_cast<double>(end.QuadPart - frameStart_.QuadPart) / static_cast<double>(frequency.QuadPart);
frameSeconds_ += seconds;
frame_pacer::frameDrawn(window_, seconds);
} }
ourTarget_ = nullptr; ourTarget_ = nullptr;
++frames_; ++frames_;
@@ -315,14 +317,20 @@ void Engine::endRendering()
const double seconds = const double seconds =
static_cast<double>(ended.QuadPart - statsStart_.QuadPart) / static_cast<double>(frequency.QuadPart); static_cast<double>(ended.QuadPart - statsStart_.QuadPart) / static_cast<double>(frequency.QuadPart);
statsStart_ = ended; statsStart_ = ended;
const Recorder::Counters sent = recorder_->takeCounters();
if (statsLogged_ < 6 || frames_ % 3000 == 0) { if (statsLogged_ < 6 || frames_ % 3000 == 0) {
++statsLogged_; ++statsLogged_;
log::info("engine: last 300 frames: {:.0f} per second; drawTriangles {} (and {} with buffers not ours), " log::info("engine: last 300 frames: {:.0f} per second; each {:.2f} ms beginRendering to endRendering, {:.2f} ms "
"drawTrianglesFromMemory {} (and {} without indices), point sprites {}, lines {}, 2D projections {}", "of it sending, {:.0f} KB sent, {:.1f} buffer updates ({:.0f} KB); drawTriangles {} (and {} with "
300 / seconds, stats_.triangles, stats_.foreignTriangles, stats_.indexed, stats_.unindexed, "buffers not ours), drawTrianglesFromMemory {} (and {} without indices), point sprites {}, lines {}, "
stats_.sprites, stats_.lines, stats_.flat); "2D projections {}",
300 / seconds, frameSeconds_ * 1000 / 300, sent.sendSeconds * 1000 / 300,
static_cast<double>(sent.bytes) / 1024 / 300, sent.bufferUpdates / 300.0,
static_cast<double>(sent.bufferBytes) / 1024 / 300, stats_.triangles, stats_.foreignTriangles,
stats_.indexed, stats_.unindexed, stats_.sprites, stats_.lines, stats_.flat);
} }
stats_ = {}; stats_ = {};
frameSeconds_ = 0;
} }
} }
+1
View File
@@ -91,6 +91,7 @@ private:
Stats stats_; Stats stats_;
LARGE_INTEGER statsStart_{}; // when the stats' 300 frames started LARGE_INTEGER statsStart_{}; // when the stats' 300 frames started
LARGE_INTEGER frameStart_{}; // when the current frame began LARGE_INTEGER frameStart_{}; // when the current frame began
double frameSeconds_ = 0; // the stats' window frames' time, beginRendering to endRendering
bool windowFrame_ = false; // the current frame is drawn into the window bool windowFrame_ = false; // the current frame is drawn into the window
int statsLogged_ = 0; int statsLogged_ = 0;
+6 -9
View File
@@ -14,10 +14,8 @@ namespace {
// The client's own rate: its ticks already draw these frames. // The client's own rate: its ticks already draw these frames.
constexpr DWORD clientRate = 30; constexpr DWORD clientRate = 30;
// Extra frames only if a frame takes less than this share of the interval. // The client is left idle at least this share of a frame's time between frames.
constexpr double affordable = 0.75; constexpr double idleShare = 1.0 / 3.0;
// 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). // The user's frame rate, from the registry (unset or unreadable: the client's own).
DWORD read_rate() DWORD read_rate()
@@ -88,8 +86,8 @@ private:
double askedAt = 0; // when a repaint was last asked for 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 // Re-reads the setting now and then; above the client's rate, asks each window for repaints at the rate, or as often
// affordable. // as leaves the client its idle time.
void run() void run()
{ {
HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
@@ -116,9 +114,8 @@ private:
if (w.pending && now - w.askedAt < 0.25) { if (w.pending && now - w.askedAt < 0.25) {
continue; // not drawn yet (asked again after a while: the client may have skipped it) 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 // Since the last frame ended: the rest of the interval since it began, and the idle time.
: now - w.askedAt >= interval && w.frameTime < interval * affordable; if (now - w.drawnAt >= std::max(interval - w.frameTime, w.frameTime * idleShare)) {
if (due) {
w.pending = true; w.pending = true;
w.askedAt = now; w.askedAt = now;
InvalidateRect(w.window, nullptr, FALSE); InvalidateRect(w.window, nullptr, FALSE);
+4 -3
View File
@@ -6,9 +6,10 @@
// Graphics, FrameRate): 30, the client's own and the default, adds nothing; 60, 120, 240 or 360 add frames between // Graphics, FrameRate): 30, the client's own and the default, adds nothing; 60, 120, 240 or 360 add frames between
// ticks; 0 is uncapped. // ticks; 0 is uncapped.
// //
// Drawing runs on the client's main thread, with its input and interface: extra frames are only asked for when they're // The rate counts all of a window's frames, the client's own included: a window is asked for one once its last frame
// affordable. A window gets one only once its last frame is drawn, and only if its frames take less than three // is drawn and the interval since it began has passed. Drawing runs on the client's main thread, with its input and
// quarters of the interval; uncapped, the client is left idle a third of a frame's time between frames. // interface, so the client is also always left idle a third of a frame's time between frames: when the rate can't be
// reached, the client draws as many frames as that allows.
#include <windows.h> #include <windows.h>
+8
View File
@@ -35,7 +35,13 @@ void Recorder::flush(bool endsFrame)
writer_.clear(); writer_.clear();
return; return;
} }
LARGE_INTEGER start, end, frequency;
QueryPerformanceCounter(&start);
process_->send(writer_.bytes(), endsFrame); process_->send(writer_.bytes(), endsFrame);
QueryPerformanceCounter(&end);
QueryPerformanceFrequency(&frequency);
counters_.bytes += writer_.size();
counters_.sendSeconds += static_cast<double>(end.QuadPart - start.QuadPart) / static_cast<double>(frequency.QuadPart);
writer_.clear(); writer_.clear();
} }
@@ -112,6 +118,8 @@ void Recorder::updateBuffer(std::uint32_t id, const void* data, std::uint32_t st
std::uint32_t count) std::uint32_t count)
{ {
const std::size_t size = std::size_t{elementSize} * count; const std::size_t size = std::size_t{elementSize} * count;
++counters_.bufferUpdates;
counters_.bufferBytes += size;
if (std::byte* payload = add(protocol::Op::UpdateBuffer, protocol::UpdateBuffer{id, elementSize, count}, size); if (std::byte* payload = add(protocol::Op::UpdateBuffer, protocol::UpdateBuffer{id, elementSize, count}, size);
payload != nullptr && data != nullptr) { payload != nullptr && data != nullptr) {
pack(payload, data, stride, elementSize, count); pack(payload, data, stride, elementSize, count);
+12
View File
@@ -6,6 +6,7 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <span> #include <span>
#include <utility>
#include "renderer_process.hpp" #include "renderer_process.hpp"
#include "t5de/imvu/types.hpp" #include "t5de/imvu/types.hpp"
@@ -52,12 +53,23 @@ public:
std::uint32_t count); std::uint32_t count);
void destroyBuffer(std::uint32_t id); void destroyBuffer(std::uint32_t id);
// Diagnostics: what's been sent, and the time spent sending (waiting for the renderer process to take it).
struct Counters {
std::uint64_t bytes = 0;
double sendSeconds = 0;
std::uint32_t bufferUpdates = 0;
std::uint64_t bufferBytes = 0;
};
// Returns the counters since the last call.
Counters takeCounters() { return std::exchange(counters_, {}); }
private: private:
void addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags); void addPixels(protocol::Op op, std::uint32_t id, const imvu::Pixmap& pixmap, std::uint32_t flags);
std::unique_ptr<RendererProcess> process_; std::unique_ptr<RendererProcess> process_;
protocol::Writer writer_; protocol::Writer writer_;
std::uint32_t nextId_ = 1; std::uint32_t nextId_ = 1;
Counters counters_;
}; };
// Copies count elements of elementSize bytes, stride bytes apart (0: packed), into destination, packed. // Copies count elements of elementSize bytes, stride bytes apart (0: packed), into destination, packed.