diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 4610574f21..9ac0cace2d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -222,6 +222,12 @@ object NativeLibrary { external fun getDebugKnobAt(index: Int): Boolean + /** + * Publishes the refresh rate of the display the emulation surface lives on, in hertz. + * Frame generation uses it to cap the multiplier to what the display can actually present. + */ + external fun setDisplayRefreshRate(rate: Float) + /** * Set the current speed limit to the configured turbo speed. */ diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 716bdfeb7e..4575c4201f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -245,6 +245,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager override fun onResume() { super.onResume() + publishDisplayRefreshRate() nfcReader.startScanning() startMotionSensorListener() InputHandler.updateControllerData() @@ -253,6 +254,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager buildPictureInPictureParams() } + private fun publishDisplayRefreshRate() { + val rate = display?.mode?.refreshRate ?: display?.refreshRate ?: 0f + NativeLibrary.setDisplayRefreshRate(rate) + } + override fun onPause() { nfcReader.stopScanning() stopMotionSensorListener() 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 ea41fb6cbd..6029a94861 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 @@ -20,6 +20,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { RENDERER_ACCURACY("gpu_accuracy"), RENDERER_RESOLUTION("resolution_setup"), RENDERER_FRAME_GEN_MULTIPLIER("frame_gen_multiplier"), + RENDERER_FRAME_GEN_QUEUE_TARGET("frame_gen_queue_target"), RENDERER_FRAME_GEN_FLOW_SCALE("frame_gen_flow_scale"), RENDERER_VSYNC("use_vsync"), RENDERER_SCALING_FILTER("scaling_filter"), 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 d3d487ebb2..0630335d27 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 @@ -99,6 +99,7 @@ abstract class SettingsItem( private val frameGenKeys = setOf( BooleanSetting.RENDERER_FRAME_GEN.key, IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key, + IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key, IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key, BooleanSetting.RENDERER_FRAME_GEN_FP16.key, BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key @@ -638,6 +639,15 @@ abstract class SettingsItem( valuesId = R.array.frameGenMultiplierValues ) ) + put( + SingleChoiceSetting( + IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET, + titleId = R.string.frame_gen_queue_target, + descriptionId = R.string.frame_gen_queue_target_description, + choicesId = R.array.frameGenQueueTargetNames, + valuesId = R.array.frameGenQueueTargetValues + ) + ) put( SliderSetting( IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE, 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 f930ca7633..9928fe5645 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 @@ -126,6 +126,7 @@ class SettingsFragmentPresenter( add(BooleanSetting.RENDERER_FRAME_GEN.key) add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key) + add(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key) add(IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key) add(BooleanSetting.RENDERER_FRAME_GEN_FP16.key) } diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp index 8dfea40693..d2020ce98d 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.cpp +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "common/android/id_cache.h" @@ -33,8 +34,12 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { m_pending_frame_rate_hint = -1.0f; m_pending_frame_rate_hint_votes = 0; m_smoothed_present_rate = 0.0f; + m_peak_guest_frame_rate = 0.0f; + m_published_guest_frame_rate = 0.0f; + Settings::values.guest_frame_rate = 0.0f; m_last_frame_display_time = {}; m_pending_frame_rate_since = {}; + m_guest_frame_rate_since = {}; return; } @@ -64,8 +69,28 @@ void EmuWindow_Android::OnTouchReleased(int id) { EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(id); } +namespace { + +constexpr std::array GUEST_FRAME_RATE_LADDER{20.0f, 30.0f, 60.0f}; + +[[nodiscard]] float SnapToGuestFrameRateLadder(float rate) { + float snapped = GUEST_FRAME_RATE_LADDER.front(); + float best_distance = std::numeric_limits::max(); + for (const float step : GUEST_FRAME_RATE_LADDER) { + const float distance = std::fabs(step - rate); + if (distance <= best_distance) { + best_distance = distance; + snapped = step; + } + } + return snapped; +} + +} // Anonymous namespace + void EmuWindow_Android::OnFrameDisplayed() { UpdateObservedFrameRate(); + UpdateGuestFrameRate(); UpdateFrameRateHint(); if (!m_first_frame) { @@ -97,6 +122,40 @@ void EmuWindow_Android::UpdateObservedFrameRate() { m_last_frame_display_time = now; } +void EmuWindow_Android::UpdateGuestFrameRate() { + const float measured = GetFrameTimeVerifiedHint(); + if (measured <= 0.0f) { + return; + } + + constexpr float PeakDecayFactor = 0.0015f; + m_peak_guest_frame_rate = + measured > m_peak_guest_frame_rate + ? measured + : m_peak_guest_frame_rate + (measured - m_peak_guest_frame_rate) * PeakDecayFactor; + + const float candidate = SnapToGuestFrameRateLadder(m_peak_guest_frame_rate); + if (candidate == m_published_guest_frame_rate) { + m_guest_frame_rate_since = {}; + return; + } + + const auto now = Clock::now(); + if (m_guest_frame_rate_since.time_since_epoch().count() == 0) { + m_guest_frame_rate_since = now; + return; + } + + constexpr auto SettleDuration = std::chrono::seconds(3); + if (now - m_guest_frame_rate_since < SettleDuration) { + return; + } + + m_published_guest_frame_rate = candidate; + Settings::values.guest_frame_rate = candidate; + m_guest_frame_rate_since = {}; +} + float EmuWindow_Android::QuantizeFrameRateHint(float frame_rate) { if (!std::isfinite(frame_rate) || frame_rate <= 0.0f) { return 0.0f; @@ -128,7 +187,7 @@ float EmuWindow_Android::GetPresentedFrameMultiplier() { if (!Settings::values.frame_gen.GetValue()) { return 1.0f; } - return static_cast(std::clamp(Settings::values.frame_gen_multiplier.GetValue(), 2, 4)); + return static_cast(Settings::FrameGenMultiplier()); } float EmuWindow_Android::GetFrameRateHint() const { diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h index fca83b91fa..33b8f9ba83 100644 --- a/src/android/app/src/main/jni/emu_window/emu_window.h +++ b/src/android/app/src/main/jni/emu_window/emu_window.h @@ -59,6 +59,7 @@ private: void UpdateFrameRateHint(); void UpdateObservedFrameRate(); + void UpdateGuestFrameRate(); [[nodiscard]] float GetFrameRateHint() const; [[nodiscard]] float GetFrameTimeVerifiedHint() const; [[nodiscard]] static float GetPresentedFrameMultiplier(); @@ -73,7 +74,10 @@ private: float m_last_frame_rate_hint = -1.0f; float m_pending_frame_rate_hint = -1.0f; float m_smoothed_present_rate = 0.0f; + float m_peak_guest_frame_rate = 0.0f; + float m_published_guest_frame_rate = 0.0f; Clock::time_point m_last_frame_display_time{}; Clock::time_point m_pending_frame_rate_since{}; + Clock::time_point m_guest_frame_rate_since{}; std::uint32_t m_pending_frame_rate_hint_votes = 0; }; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 7173ccddd0..80b2991722 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -1229,6 +1229,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobje return static_cast(Settings::getDebugKnobAt(static_cast(index))); } +void Java_org_yuzu_yuzu_1emu_NativeLibrary_setDisplayRefreshRate(JNIEnv* env, jobject jobj, + jfloat rate) { + Settings::values.display_refresh_rate = static_cast(rate); +} + void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) { if (enabled) { Settings::values.use_speed_limit.SetValue(true); diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 2778e76ee7..d63435331d 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -163,17 +163,31 @@ + @string/frame_gen_multiplier_auto @string/frame_gen_multiplier_2x @string/frame_gen_multiplier_3x @string/frame_gen_multiplier_4x + 0 2 3 4 + + @string/frame_gen_queue_target_0 + @string/frame_gen_queue_target_1 + @string/frame_gen_queue_target_2 + + + + 0 + 1 + 2 + + @string/renderer_vsync_immediate @string/renderer_vsync_mailbox diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 954c376446..b1ba713f55 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -302,10 +302,16 @@ Manage and configure frame generation Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled. Frame multiplier - How many frames to display for each rendered frame. Higher values cost proportionally more GPU time. + How many frames to display for each rendered frame. Higher values cost proportionally more GPU time. Capped to what your display can present at the game\'s frame rate. + Auto 2x 3x 4x + Frame queue target + How many finished frames may wait ahead of the display. Larger queues absorb GPU spikes at the cost of input latency. + Unbuffered (lowest latency) + One frame (balanced) + Two frames (smoothest) Motion estimation resolution Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance. Half precision shaders diff --git a/src/common/settings.cpp b/src/common/settings.cpp index db318613dd..71fc0112d6 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -380,6 +380,43 @@ void UpdateRescalingInfo() { TranslateResolutionInfo(setup, info); } +u32 FrameGenMultiplierCeiling() { + const u32 configured = values.frame_gen_multiplier.GetValue(); + if (configured == AUTO_FRAME_GEN_MULTIPLIER) { + return MAX_FRAME_GEN_MULTIPLIER; + } + return std::clamp(configured, MIN_FRAME_GEN_MULTIPLIER, MAX_FRAME_GEN_MULTIPLIER); +} + +u32 FrameGenMultiplier() { + const u32 ceiling = FrameGenMultiplierCeiling(); + + const float refresh = values.display_refresh_rate; + if (refresh <= 1.0f) { + return ceiling; + } + + constexpr float UNMEASURED_GUEST_FRAME_RATE = 60.0f; + float base = values.guest_frame_rate > 1.0f ? values.guest_frame_rate + : UNMEASURED_GUEST_FRAME_RATE; + if (values.use_speed_limit.GetValue()) { + const u16 limit = std::min(SpeedLimit(), 100); + if (limit > 0) { + base *= static_cast(limit) / 100.0f; + } + } + + const u32 presentable = static_cast(refresh / std::max(base, 1.0f)); + return std::min(ceiling, std::max(presentable, 1u)); +} + +size_t FrameGenGenerations() { + if (!values.frame_gen.GetValue()) { + return 0; + } + return FrameGenMultiplier() - 1; +} + void RestoreGlobalState(bool is_powered_on) { // If a game is running, DO NOT restore the global settings state if (is_powered_on) { diff --git a/src/common/settings.h b/src/common/settings.h index 4466714fdf..8513d5e8ea 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -392,8 +392,8 @@ struct Values { Specialization::Default, true, false}; SwitchableSetting frame_gen_multiplier{linkage, - 2, - 2, + 0, + 0, 4, "frame_gen_multiplier", Category::Renderer, @@ -414,6 +414,17 @@ struct Values { true, &frame_gen}; + SwitchableSetting frame_gen_queue_target{linkage, + 1, + 0, + 2, + "frame_gen_queue_target", + Category::Renderer, + Specialization::Countable, + true, + false, + &frame_gen}; + SwitchableSetting frame_gen_fp16{linkage, true, "frame_gen_fp16", Category::Renderer, Specialization::Default, true, false, &frame_gen}; @@ -902,10 +913,24 @@ struct Values { // Per-game overrides bool use_squashed_iterated_blend; + + // Rates published by the frontend, in hertz. Zero means not measured yet. + float display_refresh_rate; + float guest_frame_rate; }; extern Values values; +constexpr u32 AUTO_FRAME_GEN_MULTIPLIER = 0; +constexpr u32 MIN_FRAME_GEN_MULTIPLIER = 2; +constexpr u32 MAX_FRAME_GEN_MULTIPLIER = 4; + +[[nodiscard]] u32 FrameGenMultiplierCeiling(); + +[[nodiscard]] u32 FrameGenMultiplier(); + +[[nodiscard]] size_t FrameGenGenerations(); + bool getDebugKnobAt(u8 i); void UpdateGPUAccuracy(); diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp index 04fa6857d3..76d9834b1b 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.cpp +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -27,9 +27,7 @@ constexpr u64 LSFG_REQUIRED_FRAMES = 2; } [[nodiscard]] size_t ConfiguredGenerations() { - const u32 multiplier = std::clamp(Settings::values.frame_gen_multiplier.GetValue(), - LSFG_MIN_MULTIPLIER, LSFG_MAX_MULTIPLIER); - return multiplier - 1; + return Settings::FrameGenGenerations(); } bool IsBlueFirst(VkFormat format) { @@ -232,7 +230,7 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, bool } size_t FrameGen::WantedGenerations() const { - if (unavailable || !Settings::values.frame_gen.GetValue()) { + if (unavailable) { return 0; } return ConfiguredGenerations(); diff --git a/src/video_core/renderer_vulkan/present/lsfg_common.h b/src/video_core/renderer_vulkan/present/lsfg_common.h index cf683cd138..c2114de2b4 100644 --- a/src/video_core/renderer_vulkan/present/lsfg_common.h +++ b/src/video_core/renderer_vulkan/present/lsfg_common.h @@ -27,9 +27,6 @@ constexpr VkFormat LSFG_FLOW_FORMAT = VK_FORMAT_R8_UNORM; constexpr VkFormat LSFG_MOTION_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT; constexpr size_t LSFG_HISTORY_SLOTS = 3; -constexpr size_t LSFG_MIN_MULTIPLIER = 2; -constexpr size_t LSFG_MAX_MULTIPLIER = 4; -constexpr size_t LSFG_MAX_GENERATIONS = LSFG_MAX_MULTIPLIER - 1; constexpr size_t LSFG_MAX_TARGETS = 7; [[nodiscard]] constexpr f32 LsfgTimestamp(size_t generation, size_t generation_count) { diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index d7c441fd6a..a5f08698f8 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -351,14 +351,13 @@ void PresentManager::SetImageCount() { // We cannot have more than 7 images in flight at any given time. // FRAMES_IN_FLIGHT is 8, and the cache TICKS_TO_DESTROY is 8. // Mali drivers will give us 6. - const size_t generations = - Settings::values.frame_gen.GetValue() - ? static_cast(Settings::values.frame_gen_multiplier.GetValue()) - 1 - : 0; - const size_t frames_per_composite = generations + 1; - image_count = std::min( - std::max(swapchain.GetImageCount() + generations, frames_per_composite * 2), - MAX_FRAMES_IN_FLIGHT); + const size_t generations = Settings::values.frame_gen.GetValue() + ? Settings::FrameGenMultiplierCeiling() - 1 + : 0; + const size_t queued_composites = Settings::values.frame_gen_queue_target.GetValue() + 1; + image_count = + std::clamp((generations + 1) * queued_composites, swapchain.GetImageCount(), + MAX_FRAMES_IN_FLIGHT); } void PresentManager::CopyToSwapchain(Frame* frame) {