mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[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:
@@ -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");
|
||||
|
||||
@@ -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] = {};
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user