diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 0bc18b338..421aa7834 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -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(framerate_limit); + const uint64_t tick_freq = Clock::guest_tick_frequency(); + + const uint64_t target_duration_ticks = static_cast( + target_duration_ms * static_cast(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(time_delta) / + (static_cast(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(remaining_ms * 1000000.0 * 0.90); + if (sleep_ns > 0) { + xe::threading::NanoSleep(sleep_ns); + } +#else + const uint64_t sleep_ns = static_cast(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"); diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index 6caaf55ed..d3f806630 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -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] = {}; diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index c21dd803b..d25d64dca 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -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. " diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index 77909f6c4..3fab44c76 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -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); diff --git a/src/xenia/gpu/graphics_system.cc b/src/xenia/gpu/graphics_system.cc index e4013998e..acd4a4964 100644 --- a/src/xenia/gpu/graphics_system.cc +++ b/src/xenia/gpu/graphics_system.cc @@ -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(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( - 5.0, 1000.0 / static_cast( - 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( + (1000000000.0 / static_cast(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(time_delta) / - (static_cast(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( - (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(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; }, diff --git a/src/xenia/gpu/pm4_command_processor_implement.h b/src/xenia/gpu/pm4_command_processor_implement.h index 8c1276347..77a2feedb 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -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(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);