From 5f676a6a55aa4d748e545b0fc6956e9a5dbd2f7f Mon Sep 17 00:00:00 2001 From: MaranBr Date: Thu, 12 Feb 2026 00:23:19 +0100 Subject: [PATCH] [vulkan] Add support for target FPS frame pacing (#3494) This allows users to choose how the emulator manages frame pacing to reduce stuttering and provide a smoother and more consistent frame rate. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3494 Reviewed-by: CamilleLaVey Co-authored-by: MaranBr Co-committed-by: MaranBr --- .../features/settings/model/IntSetting.kt | 1 + .../settings/model/view/SettingsItem.kt | 9 ++++ .../settings/ui/SettingsFragmentPresenter.kt | 1 + .../app/src/main/res/values/arrays.xml | 15 +++++++ .../app/src/main/res/values/strings.xml | 9 ++++ src/common/settings.h | 7 ++++ src/common/settings_enums.h | 1 + src/qt_common/config/shared_translation.cpp | 10 +++++ .../renderer_vulkan/vk_scheduler.cpp | 2 +- src/video_core/renderer_vulkan/vk_scheduler.h | 32 +++++++++++--- .../renderer_vulkan/vk_swapchain.cpp | 42 ++++++++++++++++++- 11 files changed, 121 insertions(+), 8 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index be3b2f4a48..c8985cd45d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -29,6 +29,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { RENDERER_DYNA_STATE("dyna_state"), DMA_ACCURACY("dma_accuracy"), + FRAME_PACING_MODE("frame_pacing_mode"), AUDIO_OUTPUT_ENGINE("output_engine"), MAX_ANISOTROPY("max_anisotropy"), THEME("theme"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index acbfbac337..762f0da262 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -643,6 +643,15 @@ abstract class SettingsItem( valuesId = R.array.dmaAccuracyValues ) ) + put( + SingleChoiceSetting( + IntSetting.FRAME_PACING_MODE, + titleId = R.string.frame_pacing_mode, + descriptionId = R.string.frame_pacing_mode_description, + choicesId = R.array.framePacingModeNames, + valuesId = R.array.framePacingModeValues + ) + ) put( SwitchSetting( BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 5ea94d3ac5..06d4d74176 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -265,6 +265,7 @@ class SettingsFragmentPresenter( add(IntSetting.RENDERER_ACCURACY.key) add(IntSetting.DMA_ACCURACY.key) + add(IntSetting.FRAME_PACING_MODE.key) add(IntSetting.MAX_ANISOTROPY.key) add(IntSetting.RENDERER_VRAM_USAGE_MODE.key) add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key) diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 69f1590844..e85bc3592a 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -530,6 +530,21 @@ 2 + + @string/frame_pacing_mode_target_Auto + @string/frame_pacing_mode_target_30 + @string/frame_pacing_mode_target_60 + @string/frame_pacing_mode_target_120 + @string/frame_pacing_mode_target_240 + + + 0 + 1 + 2 + 3 + 4 + + @string/applet_hle @string/applet_lle diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 531b14789c..3d6a27bb3c 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -473,6 +473,8 @@ Controls the GPU emulation mode. Most games render fine with Fast or Balanced modes, but Accurate is still required for some. Particles tend to only render correctly with Accurate mode. DMA Accuracy Controls the DMA precision accuracy. Safe precision can fix issues in some games, but it can also impact performance in some cases. If unsure, leave this on Default. + Frame Pacing Mode + Controls how the emulator manages frame pacing to reduce stuttering and make the frame rate smoother and more consistent. Anisotropic filtering Improves the quality of textures when viewed at oblique angles VRAM Usage Mode @@ -1028,6 +1030,13 @@ Unsafe Safe + + Auto + 30 FPS + 60 FPS + 120 FPS + 240 FPS + CPU GPU diff --git a/src/common/settings.h b/src/common/settings.h index 770acdbb69..385ae7e1c9 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -434,6 +434,13 @@ struct Values { "accelerate_astc", Category::RendererAdvanced}; + SwitchableSetting frame_pacing_mode{linkage, + FramePacingMode::Target_Auto, + FramePacingMode::Target_Auto, + FramePacingMode::Target_240, + "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..7e3bef9bea 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, Target_Auto, Target_30, Target_60, Target_120, Target_240); 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 404685b36c..225cdcd10e 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:"), @@ -502,6 +504,14 @@ 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, Target_Auto, tr("Auto")), + PAIR(FramePacingMode, Target_30, tr("30 FPS")), + PAIR(FramePacingMode, Target_60, tr("60 FPS")), + PAIR(FramePacingMode, Target_120, tr("120 FPS")), + PAIR(FramePacingMode, Target_240, tr("240 FPS")), + }}); translations->insert({Settings::EnumMetadata::Index(), { PAIR(VramUsageMode, Conservative, tr("Conservative")), diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index aafcfdf65b..2a69d6d244 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -347,7 +347,7 @@ void Scheduler::EndRenderPass() Record([num_images = num_renderpass_images, images = renderpass_images, ranges = renderpass_image_ranges](vk::CommandBuffer cmdbuf) { - std::array barriers; + std::vector barriers(num_images); VkPipelineStageFlags src_stages = 0; for (size_t i = 0; i < num_images; ++i) { const VkImageSubresourceRange& range = ranges[i]; diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index 5216a436c8..fb0ac9b008 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project @@ -16,6 +16,7 @@ #include "common/alignment.h" #include "common/common_types.h" +#include "common/settings.h" #include "common/polyfill_thread.h" #include "video_core/renderer_vulkan/vk_master_semaphore.h" #include "video_core/vulkan_common/vulkan_wrapper.h" @@ -111,15 +112,34 @@ public: return master_semaphore->IsFree(tick); } - /// Waits for the given tick to trigger on the GPU. - void Wait(u64 tick) { - if (tick >= master_semaphore->CurrentTick()) { - // Make sure we are not waiting for the current tick without signalling + /// Waits for the given GPU tick, optionally pacing frames. + void Wait(u64 tick, double target_fps = 0.0) { + if (Settings::values.use_speed_limit.GetValue() && target_fps > 0.0) { + auto frame_duration = std::chrono::duration_cast(std::chrono::duration(1.0 / target_fps)); + auto now = std::chrono::steady_clock::now(); + if (now < next_frame_time) { + std::this_thread::sleep_until(next_frame_time); + next_frame_time += frame_duration; + } else { + next_frame_time = now + frame_duration; + } + } + if (tick > master_semaphore->CurrentTick() && !chunk->Empty()) { Flush(); } master_semaphore->Wait(tick); } + /// Resets the frame pacing state by setting the next frame time. + void ResetFramePacing(double target_fps = 0.0) { + if (target_fps > 0.0) { + auto frame_duration = std::chrono::duration_cast(std::chrono::duration(1.0 / target_fps)); + next_frame_time = std::chrono::steady_clock::now() + frame_duration; + } else { + next_frame_time = std::chrono::steady_clock::time_point{}; + } + } + /// Returns the master timeline semaphore. [[nodiscard]] MasterSemaphore& GetMasterSemaphore() const noexcept { return *master_semaphore; @@ -261,6 +281,8 @@ private: std::mutex queue_mutex; std::condition_variable_any event_cv; std::jthread worker_thread; + + std::chrono::steady_clock::time_point next_frame_time{}; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 7418ad934e..7e7e67639d 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project @@ -146,6 +146,25 @@ void Swapchain::Create( { is_outdated = false; is_suboptimal = false; + + switch (Settings::values.frame_pacing_mode.GetValue()) { + case Settings::FramePacingMode::Target_Auto: + scheduler.ResetFramePacing(); + break; + case Settings::FramePacingMode::Target_30: + scheduler.ResetFramePacing(30.0); + break; + case Settings::FramePacingMode::Target_60: + scheduler.ResetFramePacing(60.0); + break; + case Settings::FramePacingMode::Target_120: + scheduler.ResetFramePacing(120.0); + break; + case Settings::FramePacingMode::Target_240: + scheduler.ResetFramePacing(240.0); + break; + } + width = width_; height = height_; #ifdef ANDROID @@ -194,7 +213,26 @@ bool Swapchain::AcquireNextImage() { break; } - scheduler.Wait(resource_ticks[image_index]); + if (resource_ticks[image_index] != 0 && !scheduler.IsFree(resource_ticks[image_index])) { + switch (Settings::values.frame_pacing_mode.GetValue()) { + case Settings::FramePacingMode::Target_Auto: + scheduler.Wait(resource_ticks[image_index]); + break; + case Settings::FramePacingMode::Target_30: + scheduler.Wait(resource_ticks[image_index], 30.0); + break; + case Settings::FramePacingMode::Target_60: + scheduler.Wait(resource_ticks[image_index], 60.0); + break; + case Settings::FramePacingMode::Target_120: + scheduler.Wait(resource_ticks[image_index], 120.0); + break; + case Settings::FramePacingMode::Target_240: + scheduler.Wait(resource_ticks[image_index], 240.0); + break; + } + } + resource_ticks[image_index] = scheduler.CurrentTick(); return is_suboptimal || is_outdated;