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 a20858f0a9..6f25856cbf 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 @@ -670,15 +670,6 @@ 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 853e1e3e4b..060a6fe9ae 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 @@ -256,7 +256,6 @@ 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/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 02368bfc16..584322df50 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -86,8 +86,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { binding = ActivityMainBinding.inflate(layoutInflater) - // Since Android 15, google automatically forces "games" to be 60 hrz - // This ensures the display's max refresh rate is actually used display?.let { val supportedModes = it.supportedModes val maxRefreshRate = supportedModes.maxByOrNull { mode -> mode.refreshRate } 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 4e6fd560f4..6886dac172 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 @@ -6,8 +6,15 @@ #include +#include +#include +#include +#include +#include + #include "common/android/id_cache.h" #include "common/logging.h" +#include "common/settings.h" #include "input_common/drivers/android.h" #include "input_common/drivers/touch_screen.h" #include "input_common/drivers/virtual_amiibo.h" @@ -22,6 +29,12 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { m_window_width = 0; m_window_height = 0; window_info.render_surface = nullptr; + m_last_frame_rate_hint = -1.0f; + m_pending_frame_rate_hint = -1.0f; + m_pending_frame_rate_hint_votes = 0; + m_smoothed_present_rate = 0.0f; + m_last_frame_display_time = {}; + m_pending_frame_rate_since = {}; return; } @@ -32,6 +45,7 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { UpdateCurrentFramebufferLayout(m_window_width, m_window_height); window_info.render_surface = reinterpret_cast(surface); + UpdateFrameRateHint(); } void EmuWindow_Android::OnTouchPressed(int id, float x, float y) { @@ -51,6 +65,9 @@ void EmuWindow_Android::OnTouchReleased(int id) { } void EmuWindow_Android::OnFrameDisplayed() { + UpdateObservedFrameRate(); + UpdateFrameRateHint(); + if (!m_first_frame) { Common::Android::RunJNIOnFiber( [&](JNIEnv* env) { EmulationSession::GetInstance().OnEmulationStarted(); }); @@ -58,6 +75,175 @@ void EmuWindow_Android::OnFrameDisplayed() { } } +void EmuWindow_Android::UpdateObservedFrameRate() { + const auto now = Clock::now(); + if (m_last_frame_display_time.time_since_epoch().count() != 0) { + const auto frame_time = std::chrono::duration(now - m_last_frame_display_time); + const float seconds = frame_time.count(); + if (seconds > 0.0f) { + const float instantaneous_rate = 1.0f / seconds; + if (std::isfinite(instantaneous_rate) && instantaneous_rate >= 1.0f && + instantaneous_rate <= 240.0f) { + constexpr float SmoothingFactor = 0.15f; + if (m_smoothed_present_rate <= 0.0f) { + m_smoothed_present_rate = instantaneous_rate; + } else { + m_smoothed_present_rate += + (instantaneous_rate - m_smoothed_present_rate) * SmoothingFactor; + } + } + } + } + m_last_frame_display_time = now; +} + +float EmuWindow_Android::QuantizeFrameRateHint(float frame_rate) { + if (!std::isfinite(frame_rate) || frame_rate <= 0.0f) { + return 0.0f; + } + + frame_rate = std::clamp(frame_rate, 1.0f, 240.0f); + + constexpr float Step = 0.5f; + return std::round(frame_rate / Step) * Step; +} + +float EmuWindow_Android::GetFrameTimeVerifiedHint() const { + if (!EmulationSession::GetInstance().IsRunning()) { + return 0.0f; + } + + const double frame_time_scale = + EmulationSession::GetInstance().System().GetPerfStats().GetLastFrameTimeScale(); + if (!std::isfinite(frame_time_scale) || frame_time_scale <= 0.0) { + return 0.0f; + } + + const float verified_rate = + std::clamp(60.0f / static_cast(frame_time_scale), 0.0f, 240.0f); + return QuantizeFrameRateHint(verified_rate); +} + +float EmuWindow_Android::GetFrameRateHint() const { + const float observed_rate = std::clamp(m_smoothed_present_rate, 0.0f, 240.0f); + const float frame_time_verified_hint = GetFrameTimeVerifiedHint(); + + if (m_last_frame_rate_hint > 0.0f && observed_rate > 0.0f) { + const float tolerance = std::max(m_last_frame_rate_hint * 0.12f, 4.0f); + if (std::fabs(observed_rate - m_last_frame_rate_hint) <= tolerance) { + return m_last_frame_rate_hint; + } + } + + const float observed_hint = QuantizeFrameRateHint(observed_rate); + if (observed_hint > 0.0f) { + if (frame_time_verified_hint > 0.0f) { + const float tolerance = std::max(observed_hint * 0.20f, 3.0f); + if (std::fabs(observed_hint - frame_time_verified_hint) <= tolerance) { + return QuantizeFrameRateHint((observed_hint + frame_time_verified_hint) * 0.5f); + } + } + return observed_hint; + } + + if (frame_time_verified_hint > 0.0f) { + return frame_time_verified_hint; + } + + constexpr float NominalFrameRate = 60.0f; + if (!Settings::values.use_speed_limit.GetValue()) { + return NominalFrameRate; + } + + const u16 speed_limit = Settings::SpeedLimit(); + if (speed_limit == 0) { + return 0.0f; + } + + const float speed_limited_rate = + NominalFrameRate * (static_cast(std::min(speed_limit, 100)) / 100.0f); + return QuantizeFrameRateHint(speed_limited_rate); +} + +void EmuWindow_Android::UpdateFrameRateHint() { + auto* const surface = reinterpret_cast(window_info.render_surface); + if (!surface) { + return; + } + + const auto now = Clock::now(); + const float frame_rate_hint = GetFrameRateHint(); + if (std::fabs(frame_rate_hint - m_last_frame_rate_hint) < 0.01f) { + m_pending_frame_rate_hint = frame_rate_hint; + m_pending_frame_rate_hint_votes = 0; + m_pending_frame_rate_since = {}; + return; + } + + if (frame_rate_hint == 0.0f) { + m_pending_frame_rate_hint = frame_rate_hint; + m_pending_frame_rate_hint_votes = 0; + m_pending_frame_rate_since = now; + } else if (m_last_frame_rate_hint >= 0.0f) { + if (std::fabs(frame_rate_hint - m_pending_frame_rate_hint) >= 0.01f) { + m_pending_frame_rate_hint = frame_rate_hint; + m_pending_frame_rate_hint_votes = 1; + m_pending_frame_rate_since = now; + return; + } + + ++m_pending_frame_rate_hint_votes; + if (m_pending_frame_rate_since.time_since_epoch().count() == 0) { + m_pending_frame_rate_since = now; + } + + const auto stable_for = now - m_pending_frame_rate_since; + const float reference_rate = std::max(frame_rate_hint, 1.0f); + const auto stable_duration = std::chrono::duration_cast( + std::chrono::duration(std::clamp(3.0f / reference_rate, 0.15f, 0.40f))); + constexpr std::uint32_t MinStableVotes = 3; + + if (m_pending_frame_rate_hint_votes < MinStableVotes || stable_for < stable_duration) { + return; + } + } else { + m_pending_frame_rate_since = now; + } + + using SetFrameRateWithChangeStrategyFn = + int32_t (*)(ANativeWindow*, float, int8_t, int8_t); + using SetFrameRateFn = int32_t (*)(ANativeWindow*, float, int8_t); + static const auto set_frame_rate_with_change_strategy = + reinterpret_cast( + dlsym(RTLD_DEFAULT, "ANativeWindow_setFrameRateWithChangeStrategy")); + static const auto set_frame_rate = reinterpret_cast( + dlsym(RTLD_DEFAULT, "ANativeWindow_setFrameRate")); + + constexpr int8_t FrameRateCompatibilityDefault = 0; + constexpr int8_t ChangeFrameRateOnlyIfSeamless = 0; + + int32_t result = -1; + if (set_frame_rate_with_change_strategy) { + result = set_frame_rate_with_change_strategy(surface, frame_rate_hint, + FrameRateCompatibilityDefault, + ChangeFrameRateOnlyIfSeamless); + } else if (set_frame_rate) { + result = set_frame_rate(surface, frame_rate_hint, FrameRateCompatibilityDefault); + } else { + return; + } + + if (result != 0) { + LOG_DEBUG(Frontend, "Failed to update Android surface frame rate hint: {}", result); + return; + } + + m_last_frame_rate_hint = frame_rate_hint; + m_pending_frame_rate_hint = frame_rate_hint; + m_pending_frame_rate_hint_votes = 0; + m_pending_frame_rate_since = {}; +} + EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, std::shared_ptr driver_library) : m_driver_library{driver_library} { 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 d7b5fc6dac..b73e8b9b4d 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 @@ -1,8 +1,13 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include +#include #include #include @@ -50,10 +55,24 @@ public: }; private: + using Clock = std::chrono::steady_clock; + + void UpdateFrameRateHint(); + void UpdateObservedFrameRate(); + [[nodiscard]] float GetFrameRateHint() const; + [[nodiscard]] float GetFrameTimeVerifiedHint() const; + [[nodiscard]] static float QuantizeFrameRateHint(float frame_rate); + float m_window_width{}; float m_window_height{}; std::shared_ptr m_driver_library; bool m_first_frame = false; + float m_last_frame_rate_hint = -1.0f; + float m_pending_frame_rate_hint = -1.0f; + float m_smoothed_present_rate = 0.0f; + Clock::time_point m_last_frame_display_time{}; + Clock::time_point m_pending_frame_rate_since{}; + std::uint32_t m_pending_frame_rate_hint_votes = 0; }; diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 5d55cf551b..0e4c274d94 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -9,6 +9,10 @@ #include #include +#ifdef __ANDROID__ +#include +#endif + #include "common/logging.h" #include "common/settings.h" #include "common/settings_enums.h" @@ -170,6 +174,7 @@ bool Swapchain::AcquireNextImage() { break; } + const auto wait_with_frame_pacing = [this] { switch (Settings::values.frame_pacing_mode.GetValue()) { case Settings::FramePacingMode::Target_Auto: scheduler.Wait(resource_ticks[image_index]); @@ -187,6 +192,17 @@ bool Swapchain::AcquireNextImage() { scheduler.Wait(resource_ticks[image_index], 120.0); break; } + }; + +#ifdef __ANDROID__ + if (android_get_device_api_level() >= 30) { + scheduler.Wait(resource_ticks[image_index]); + } else { + wait_with_frame_pacing(); + } +#else + wait_with_frame_pacing(); +#endif resource_ticks[image_index] = scheduler.CurrentTick();