Queues and refresh rate adjustments

This commit is contained in:
CamilleLaVey
2026-08-13 17:26:52 -04:00
parent 1428abe1d1
commit 9b20c6c609
15 changed files with 187 additions and 19 deletions
@@ -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.
*/
@@ -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()
@@ -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"),
@@ -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,
@@ -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)
}
@@ -10,6 +10,7 @@
#include <array>
#include <cmath>
#include <cstdint>
#include <limits>
#include <dlfcn.h>
#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<float>::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<float>(std::clamp<u32>(Settings::values.frame_gen_multiplier.GetValue(), 2, 4));
return static_cast<float>(Settings::FrameGenMultiplier());
}
float EmuWindow_Android::GetFrameRateHint() const {
@@ -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;
};
+5
View File
@@ -1229,6 +1229,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobje
return static_cast<jboolean>(Settings::getDebugKnobAt(static_cast<u8>(index)));
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setDisplayRefreshRate(JNIEnv* env, jobject jobj,
jfloat rate) {
Settings::values.display_refresh_rate = static_cast<float>(rate);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
if (enabled) {
Settings::values.use_speed_limit.SetValue(true);
@@ -163,17 +163,31 @@
</string-array>
<string-array name="frameGenMultiplierNames">
<item>@string/frame_gen_multiplier_auto</item>
<item>@string/frame_gen_multiplier_2x</item>
<item>@string/frame_gen_multiplier_3x</item>
<item>@string/frame_gen_multiplier_4x</item>
</string-array>
<integer-array name="frameGenMultiplierValues">
<item>0</item>
<item>2</item>
<item>3</item>
<item>4</item>
</integer-array>
<string-array name="frameGenQueueTargetNames">
<item>@string/frame_gen_queue_target_0</item>
<item>@string/frame_gen_queue_target_1</item>
<item>@string/frame_gen_queue_target_2</item>
</string-array>
<integer-array name="frameGenQueueTargetValues">
<item>0</item>
<item>1</item>
<item>2</item>
</integer-array>
<string-array name="rendererVSyncNames">
<item>@string/renderer_vsync_immediate</item>
<item>@string/renderer_vsync_mailbox</item>
@@ -302,10 +302,16 @@
<string name="frame_gen_submenu_description">Manage and configure frame generation</string>
<string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string>
<string name="frame_gen_multiplier">Frame multiplier</string>
<string name="frame_gen_multiplier_description">How many frames to display for each rendered frame. Higher values cost proportionally more GPU time.</string>
<string name="frame_gen_multiplier_description">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.</string>
<string name="frame_gen_multiplier_auto">Auto</string>
<string name="frame_gen_multiplier_2x">2x</string>
<string name="frame_gen_multiplier_3x">3x</string>
<string name="frame_gen_multiplier_4x">4x</string>
<string name="frame_gen_queue_target">Frame queue target</string>
<string name="frame_gen_queue_target_description">How many finished frames may wait ahead of the display. Larger queues absorb GPU spikes at the cost of input latency.</string>
<string name="frame_gen_queue_target_0">Unbuffered (lowest latency)</string>
<string name="frame_gen_queue_target_1">One frame (balanced)</string>
<string name="frame_gen_queue_target_2">Two frames (smoothest)</string>
<string name="frame_gen_flow_scale">Motion estimation resolution</string>
<string name="frame_gen_flow_scale_description">Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance.</string>
<string name="frame_gen_fp16">Half precision shaders</string>
+37
View File
@@ -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<u16>(SpeedLimit(), 100);
if (limit > 0) {
base *= static_cast<float>(limit) / 100.0f;
}
}
const u32 presentable = static_cast<u32>(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) {
+27 -2
View File
@@ -392,8 +392,8 @@ struct Values {
Specialization::Default, true, false};
SwitchableSetting<u32, true> frame_gen_multiplier{linkage,
2,
2,
0,
0,
4,
"frame_gen_multiplier",
Category::Renderer,
@@ -414,6 +414,17 @@ struct Values {
true,
&frame_gen};
SwitchableSetting<u32, true> frame_gen_queue_target{linkage,
1,
0,
2,
"frame_gen_queue_target",
Category::Renderer,
Specialization::Countable,
true,
false,
&frame_gen};
SwitchableSetting<bool> 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();
@@ -27,9 +27,7 @@ constexpr u64 LSFG_REQUIRED_FRAMES = 2;
}
[[nodiscard]] size_t ConfiguredGenerations() {
const u32 multiplier = std::clamp<u32>(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();
@@ -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) {
@@ -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<size_t>(Settings::values.frame_gen_multiplier.GetValue()) - 1
: 0;
const size_t frames_per_composite = generations + 1;
image_count = std::min<size_t>(
std::max<size_t>(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<size_t>((generations + 1) * queued_composites, swapchain.GetImageCount(),
MAX_FRAMES_IN_FLIGHT);
}
void PresentManager::CopyToSwapchain(Frame* frame) {