diff --git a/src/common/settings.h b/src/common/settings.h index 41f766a5e7..406baa1eba 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -439,6 +439,13 @@ struct Values { "accelerate_astc", Category::RendererAdvanced}; + SwitchableSetting frame_pacing_mode{linkage, + FramePacingMode::Default, + FramePacingMode::Default, + FramePacingMode::Target_120, + "frame_pacing_mode", + Category::RendererAdvanced}; + SwitchableSetting astc_recompression{linkage, AstcRecompression::Uncompressed, "astc_recompression", diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 33c553dc3c..509bd414ae 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -129,6 +129,7 @@ ENUM(TimeZone, Auto, Default, Cet, Cst6Cdt, Cuba, Eet, Egypt, Eire, Est, Est5Edt ENUM(AnisotropyMode, Automatic, Default, X2, X4, X8, X16, X32, X64, None); ENUM(AstcDecodeMode, Cpu, Gpu, CpuAsynchronous); ENUM(AstcRecompression, Uncompressed, Bc1, Bc3); +ENUM(FramePacingMode, Default, Target_30, Target_60, Target_120); ENUM(VSyncMode, Immediate, Mailbox, Fifo, FifoRelaxed); ENUM(VramUsageMode, Conservative, Aggressive); ENUM(RendererBackend, OpenGL_GLSL, Vulkan, Null, OpenGL_GLASM, OpenGL_SPIRV); diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 5d4185b47d..fdcfbea9e9 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -225,6 +225,8 @@ std::unique_ptr InitializeTranslations(QObject* parent) "intermediate format: RGBA8.\n" "BC1/BC3: The intermediate format will be recompressed to BC1 or BC3 format,\n" " saving VRAM but degrading image quality.")); + INSERT(Settings, frame_pacing_mode, tr("Frame Pacing Mode (Vulkan only)"), + tr("Controls how the emulator manages frame pacing to reduce stuttering and make the frame rate smoother and more consistent.")); INSERT(Settings, vram_usage_mode, tr("VRAM Usage Mode:"), @@ -497,6 +499,13 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) PAIR(AstcRecompression, Bc1, tr("BC1 (Low quality)")), PAIR(AstcRecompression, Bc3, tr("BC3 (Medium quality)")), }}); + translations->insert({Settings::EnumMetadata::Index(), + { + PAIR(FramePacingMode, Default, tr("Default")), + PAIR(FramePacingMode, Target_30, tr("30 FPS")), + PAIR(FramePacingMode, Target_60, tr("60 FPS")), + PAIR(FramePacingMode, Target_120, tr("120 FPS")), + }}); translations->insert({Settings::EnumMetadata::Index(), { PAIR(VramUsageMode, Conservative, tr("Conservative")), diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index 5216a436c8..5eeaa1731c 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -120,6 +120,22 @@ public: master_semaphore->Wait(tick); } + /// Waits until the next game frame based on the current game FPS. + void WaitFPS(u64 tick, double target_fps) { + if (master_semaphore->CurrentTick() >= tick) { + return; + } + static auto next_frame_time = std::chrono::steady_clock::now(); + auto frame_duration = std::chrono::duration(1.0 / target_fps); + next_frame_time += std::chrono::duration_cast(frame_duration); + auto now = std::chrono::steady_clock::now(); + if (next_frame_time > now) { + std::this_thread::sleep_until(next_frame_time); + } else { + next_frame_time = now; + } + } + /// Returns the master timeline semaphore. [[nodiscard]] MasterSemaphore& GetMasterSemaphore() const noexcept { return *master_semaphore; diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 7418ad934e..d0502f6c2b 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -194,7 +194,25 @@ bool Swapchain::AcquireNextImage() { break; } - scheduler.Wait(resource_ticks[image_index]); + if (!Settings::values.use_speed_limit.GetValue()) { + scheduler.Wait(resource_ticks[image_index]); + } else { + switch (Settings::values.frame_pacing_mode.GetValue()) { + case Settings::FramePacingMode::Default: + scheduler.Wait(resource_ticks[image_index]); + break; + case Settings::FramePacingMode::Target_30: + scheduler.WaitFPS(resource_ticks[image_index], 30.0); + break; + case Settings::FramePacingMode::Target_60: + scheduler.WaitFPS(resource_ticks[image_index], 60.0); + break; + case Settings::FramePacingMode::Target_120: + scheduler.WaitFPS(resource_ticks[image_index], 120.0); + break; + } + } + resource_ticks[image_index] = scheduler.CurrentTick(); return is_suboptimal || is_outdated;