[GPU] Rework frame rate throttling and improve vblank timing

Few related changes: remove ++counter_ that was double counting in
XE_SWAP, separated "vsync" (cvar) to move towards its intended meaning
of controlling guest refresh rate (but leaving it named vsync for now),
and change framerate_limit to actually do so at presentation time rather
than by attempting to influence through vblank rate.
This commit is contained in:
Herman S.
2026-01-08 11:48:59 +09:00
parent db20ed08c0
commit 39a65586ce
6 changed files with 136 additions and 68 deletions
+53
View File
@@ -11,9 +11,11 @@
#include "third_party/fmt/include/fmt/format.h"
#include "xenia/base/byte_stream.h"
#include "xenia/base/clock.h"
#include "xenia/base/cvar.h"
#include "xenia/base/logging.h"
#include "xenia/base/profiling.h"
#include "xenia/base/threading.h"
#include "xenia/config.h"
#include "xenia/gpu/gpu_flags.h"
#include "xenia/gpu/graphics_system.h"
@@ -337,6 +339,57 @@ void CommandProcessor::SetDesiredSwapPostEffect(
});
}
void CommandProcessor::ThrottlePresentation() {
// Host frame rate limiting based on framerate_limit cvar.
// This is separate from guest vblank timing (controlled by vsync cvar).
const uint64_t framerate_limit = cvars::framerate_limit;
if (framerate_limit == 0) {
// No host frame limiting
return;
}
const double target_duration_ms =
1000.0 / static_cast<double>(framerate_limit);
const uint64_t tick_freq = Clock::guest_tick_frequency();
const uint64_t target_duration_ticks = static_cast<uint64_t>(
target_duration_ms * static_cast<double>(tick_freq) / 1000.0);
// Spin until target duration has elapsed
while (true) {
const uint64_t current_time = Clock::QueryGuestTickCount();
const uint64_t time_delta = current_time - last_swap_time_;
if (time_delta >= target_duration_ticks) {
// If we've fallen behind by more than 2 frames, reset to catch up
if (time_delta > target_duration_ticks * 2) {
last_swap_time_ = current_time;
} else {
last_swap_time_ += target_duration_ticks;
}
return;
}
const double elapsed_ms = static_cast<double>(time_delta) /
(static_cast<double>(tick_freq) / 1000.0);
const double remaining_ms = target_duration_ms - elapsed_ms;
#if XE_PLATFORM_WIN32
// Sleep 90% of remaining, spin the rest for accuracy
const uint64_t sleep_ns =
static_cast<uint64_t>(remaining_ms * 1000000.0 * 0.90);
if (sleep_ns > 0) {
xe::threading::NanoSleep(sleep_ns);
}
#else
const uint64_t sleep_ns = static_cast<uint64_t>(remaining_ms * 1000000.0);
if (sleep_ns > 0) {
xe::threading::NanoSleep(sleep_ns);
}
#endif
}
}
void CommandProcessor::WorkerThreadMain() {
if (!SetupContext()) {
xe::FatalError("Unable to setup command processor internal state");
+8
View File
@@ -136,6 +136,11 @@ class CommandProcessor {
virtual void IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontbuffer_width,
uint32_t frontbuffer_height) {}
// Throttle presentation based on framerate_limit cvar.
// Called after IssueSwap to limit host frame rate without affecting guest
// vblank timing.
void ThrottlePresentation();
// May be called not only from the command processor thread when the command
// processor is paused, and the termination of this function may be explicitly
// awaited.
@@ -358,6 +363,9 @@ class CommandProcessor {
ReadbackResolveMode cached_readback_resolve_mode_ =
ReadbackResolveMode::kFast;
// For host frame rate limiting at IssueSwap
uint64_t last_swap_time_ = 0;
private:
reg::DC_LUT_30_COLOR gamma_ramp_256_entry_table_[256] = {};
reg::DC_LUT_PWL_DATA gamma_ramp_pwl_rgb_[128][3] = {};
+17 -5
View File
@@ -12,6 +12,9 @@
#include "xenia/base/logging.h"
#include "xenia/ui/renderdoc_api.h"
// Declared in xboxkrnl_video.cc
DECLARE_bool(use_50Hz_mode);
DEFINE_path(trace_gpu_prefix, "scratch/gpu/",
"Prefix path for GPU trace files.", "GPU");
DEFINE_bool(trace_gpu_stream, false, "Trace all GPU packets.", "GPU");
@@ -21,12 +24,19 @@ DEFINE_path(
"For shader debugging, path to dump GPU shaders to as they are compiled.",
"GPU");
DEFINE_bool(vsync, true, "Enable VSYNC.", "GPU");
DEFINE_bool(vsync, true,
"Control guest vblank timing.\n"
" true: Fixed rate vblanks (50Hz PAL, 60Hz NTSC based on "
"use_50Hz_mode).\n"
" false: Unlimited vblanks for games using delta time.",
"GPU");
DEFINE_uint64(framerate_limit, 0,
"Maximum frames per second. 0 = Unlimited frames.\n"
"Defaults to 60, when set to 0, and VSYNC is enabled.",
"GPU");
DEFINE_uint64(
framerate_limit, 0,
"Host frame rate limit in FPS. 0 = unlimited.\n"
"Throttles presentation without affecting guest vblank timing.\n"
"Guest vblanks are controlled by use_50Hz_mode (50Hz PAL, 60Hz NTSC).",
"GPU");
UPDATE_from_uint64(framerate_limit, 2024, 8, 31, 20, 60);
void SetVsync(bool value) { OVERRIDE_bool(vsync, value); }
@@ -89,6 +99,8 @@ void SetOcclusionQueryEnable(bool value) {
OVERRIDE_bool(occlusion_query_enable, value);
}
uint32_t GetGuestVblankRateHz() { return cvars::use_50Hz_mode ? 50 : 60; }
DEFINE_bool(
gpu_debug_markers, false,
"Insert debug markers into GPU command streams for tools like RenderDoc. "
+4
View File
@@ -37,6 +37,10 @@ DECLARE_bool(occlusion_query_enable);
void SetOcclusionQueryEnable(bool value);
// Returns the guest vblank rate in Hz (50 for PAL, 60 for NTSC).
// Based on use_50Hz_mode cvar.
uint32_t GetGuestVblankRateHz();
DECLARE_bool(disassemble_pm4);
DECLARE_bool(gpu_debug_markers);
+37 -55
View File
@@ -160,76 +160,58 @@ X_STATUS GraphicsSystem::Setup(cpu::Processor* processor,
#endif
while (frame_limiter_worker_running_) {
// Read cvars each frame to allow runtime changes
uint64_t normalized_framerate_limit =
std::max<uint64_t>(0, cvars::framerate_limit);
// Read vsync cvar each frame to allow runtime changes
// vsync=true: Fire vblanks at fixed rate (50Hz PAL, 60Hz NTSC)
// vsync=false: Fire vblanks limited by framerate_limit or 1ms
// Note: framerate_limit is handled separately at IssueSwap for
// host presentation throttling
bool vsync_enabled = cvars::vsync;
// If VSYNC is enabled, but frames are not limited,
// lock framerate at default value of 60
if (normalized_framerate_limit == 0 && vsync_enabled)
normalized_framerate_limit = 60;
const double vsync_duration_d =
vsync_enabled
? std::max<double>(
5.0, 1000.0 / static_cast<double>(
normalized_framerate_limit))
: 1.0;
register_file()->values[XE_GPU_REG_D1MODE_V_COUNTER] +=
GetInternalDisplayResolution().second;
#if XE_PLATFORM_WIN32
if (vsync_enabled) {
const uint64_t current_time = Clock::QueryGuestTickCount();
// Fixed vblank rate mode
const uint32_t vblank_hz = GetGuestVblankRateHz();
const uint64_t sleep_ns = static_cast<uint64_t>(
(1000000000.0 / static_cast<double>(vblank_hz)) *
duration_scalar);
#if XE_PLATFORM_WIN32
// Windows: time-gating + 90% sleep + 10% spin
const uint64_t tick_freq = Clock::guest_tick_frequency();
const uint64_t target_duration_ticks = tick_freq / vblank_hz;
const uint64_t current_time = Clock::QueryGuestTickCount();
const uint64_t time_delta = current_time - last_frame_time;
const double elapsed_d =
static_cast<double>(time_delta) /
(static_cast<double>(tick_freq) / 1000.0);
if (elapsed_d >= vsync_duration_d) {
last_frame_time = current_time;
if (time_delta >= target_duration_ticks) {
// If we've fallen behind by more than 2 frames, reset
if (time_delta > target_duration_ticks * 2) {
last_frame_time = current_time;
} else {
last_frame_time += target_duration_ticks;
}
MarkVblank();
const uint64_t estimated_nanoseconds = static_cast<uint64_t>(
(vsync_duration_d * 1000000.0) *
duration_scalar); // 1000 microseconds = 1 ms
threading::NanoSleep(estimated_nanoseconds);
threading::NanoSleep(sleep_ns);
}
}
if (!vsync_enabled) {
#else
// Linux: simplified timing to avoid oversleeping
MarkVblank();
if (normalized_framerate_limit > 0) {
// framerate_limit is over 0, vsync disabled
// - No VSYNC + limited frames defined by user
uint64_t framerate_limited_sleep_time =
1000000000 / normalized_framerate_limit;
xe::threading::NanoSleep(framerate_limited_sleep_time);
} else {
// framerate_limit is 0, vsync disabled
// - No VSYNC + unlimited frames
xe::threading::Sleep(std::chrono::milliseconds(1));
}
}
threading::NanoSleep(sleep_ns);
#endif
#if XE_PLATFORM_LINUX
// Linux: Use simplified timing logic to avoid oversleeping
MarkVblank();
if (vsync_enabled || normalized_framerate_limit > 0) {
uint64_t sleep_duration_ns =
static_cast<uint64_t>(vsync_duration_d * 1000000.0);
if (!vsync_enabled && normalized_framerate_limit > 0) {
sleep_duration_ns = 1000000000 / normalized_framerate_limit;
}
threading::NanoSleep(sleep_duration_ns);
} else {
xe::threading::Sleep(std::chrono::milliseconds(1));
// Unlimited mode (vsync=false)
MarkVblank();
if (cvars::framerate_limit > 0) {
// Cap vblanks at 2.5x framerate_limit to avoid flooding guest
const uint64_t max_vblank_hz = cvars::framerate_limit * 5 / 2;
const uint64_t sleep_ns = 1000000000 / max_vblank_hz;
threading::NanoSleep(sleep_ns);
} else {
// Truly unlimited - fire as fast as possible
threading::Sleep(std::chrono::milliseconds(1));
}
}
#endif
}
return 0;
},
@@ -786,7 +786,9 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_XE_SWAP(uint32_t packet,
COMMAND_PROCESSOR::IssueSwap(frontbuffer_ptr, frontbuffer_width,
frontbuffer_height);
++counter_;
// Apply host frame rate limiting (separate from guest vblank timing)
COMMAND_PROCESSOR::ThrottlePresentation();
return true;
}
@@ -867,22 +869,29 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_WAIT_REG_MEM(
matched = MatchValueAndRef(value & mask, ref, wait_info);
if (!matched) {
// Wait.
// Wait using the duration specified by the guest.
if (wait >= 0x100) {
PrepareForWait();
if (!cvars::vsync) {
// User wants it fast and dangerous.
// do nothing
} else {
if (cvars::vsync) {
// Fixed rate vblank mode - sleep since counter updates at 50/60Hz
#if XE_PLATFORM_WIN32
// Accurate timing: 90% sleep, 10% spin
const uint64_t wait_ms = wait / 0x100;
const uint64_t sleep_ns =
static_cast<uint64_t>(wait_ms * 1000000 * 0.90);
xe::threading::NanoSleep(sleep_ns);
#else
xe::threading::Sleep(std::chrono::milliseconds(wait / 0x100));
ReturnFromWait();
#endif
}
// Unlimited vblank mode (vsync=false) - spin since counter updates
// rapidly
ReturnFromWait();
if (!worker_running_) {
// Short-circuited exit.
return false;
}
} else {
}
}
} while (!matched);