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 6029a94861..e5a4f93fe0 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
@@ -21,6 +21,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
RENDERER_RESOLUTION("resolution_setup"),
RENDERER_FRAME_GEN_MULTIPLIER("frame_gen_multiplier"),
RENDERER_FRAME_GEN_QUEUE_TARGET("frame_gen_queue_target"),
+ RENDERER_FRAME_GEN_TARGET_RATE("frame_gen_target_rate"),
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 473f88c9e0..4195dd0e3d 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
@@ -100,6 +100,7 @@ abstract class SettingsItem(
BooleanSetting.RENDERER_FRAME_GEN.key,
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key,
IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key,
+ IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key,
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key,
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key,
BooleanSetting.RENDERER_FRAME_GEN_FP16.key,
@@ -640,6 +641,15 @@ abstract class SettingsItem(
valuesId = R.array.frameGenMultiplierValues
)
)
+ put(
+ SingleChoiceSetting(
+ IntSetting.RENDERER_FRAME_GEN_TARGET_RATE,
+ titleId = R.string.frame_gen_target_rate,
+ descriptionId = R.string.frame_gen_target_rate_description,
+ choicesId = R.array.frameGenTargetRateNames,
+ valuesId = R.array.frameGenTargetRateValues
+ )
+ )
put(
SingleChoiceSetting(
IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET,
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 b241d896f7..6efc3ca71e 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
@@ -125,7 +125,13 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.frame_gen))
add(BooleanSetting.RENDERER_FRAME_GEN.key)
- add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
+ add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
+ if (IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt(
+ getNeedsGlobalForKey(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
+ ) == 0
+ ) {
+ add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
+ }
add(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key)
add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index d8ae109e97..53dd0436dc 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -174,6 +174,24 @@
- 4
+
+ - @string/frame_gen_target_rate_off
+ - @string/frame_gen_target_rate_60
+ - @string/frame_gen_target_rate_90
+ - @string/frame_gen_target_rate_120
+ - @string/frame_gen_target_rate_144
+ - @string/frame_gen_target_rate_165
+
+
+
+ - 0
+ - 60
+ - 90
+ - 120
+ - 144
+ - 165
+
+
- @string/frame_gen_queue_target_0
- @string/frame_gen_queue_target_1
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 6d2f2af75f..43d6d56d0d 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -301,6 +301,14 @@
Frame generation
Manage and configure frame generation
Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.
+ Target frame rate
+ Aim for a fixed output rate by adjusting the multiplier as the game\'s own frame rate moves. Pick a rate your display can actually present.
+ Off (use fixed multiplier)
+ 60 fps
+ 90 fps
+ 120 fps
+ 144 fps
+ 165 fps
Frame multiplier
How many frames to display for each rendered frame. Higher values cost proportionally more GPU time. Asking for more than your display can present will slow emulation down.
2x
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 1f747c44b1..6cc976fe51 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -392,6 +392,16 @@ size_t FrameGenGenerations() {
return FrameGenMultiplier() - 1;
}
+size_t FrameGenMaxGenerations() {
+ if (!values.frame_gen.GetValue()) {
+ return 0;
+ }
+ if (values.frame_gen_target_rate.GetValue() != 0) {
+ return MAX_FRAME_GEN_MULTIPLIER - 1;
+ }
+ 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 730dbc0e02..9a8384c5db 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -423,6 +423,17 @@ struct Values {
true,
&frame_gen};
+ SwitchableSetting frame_gen_target_rate{linkage,
+ 0,
+ 0,
+ 240,
+ "frame_gen_target_rate",
+ Category::Renderer,
+ Specialization::Countable,
+ true,
+ false,
+ &frame_gen};
+
SwitchableSetting frame_gen_queue_target{linkage,
1,
0,
@@ -934,6 +945,8 @@ constexpr u32 MAX_FRAME_GEN_MULTIPLIER = 4;
[[nodiscard]] size_t FrameGenGenerations();
+[[nodiscard]] size_t FrameGenMaxGenerations();
+
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 ab1f6c50d4..20cf2d35ab 100644
--- a/src/video_core/renderer_vulkan/present/frame_gen.cpp
+++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp
@@ -43,9 +43,8 @@ constexpr u64 LSFG_REQUIRED_FRAMES = 2;
return std::clamp(stepped, 0.25f, 1.0f);
}
-[[nodiscard]] size_t ConfiguredGenerations() {
- return Settings::FrameGenGenerations();
-}
+constexpr f32 BASE_RATE_SMOOTHING = 0.1f;
+constexpr u32 GENERATION_CHANGE_VOTES = 30;
bool IsBlueFirst(VkFormat format) {
return format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB;
@@ -197,11 +196,68 @@ FrameGen::FrameGen(MemoryAllocator& memory_allocator_, Scheduler& scheduler_)
FrameGen::~FrameGen() = default;
+void FrameGen::UpdateBaseRate() {
+ const auto now = Clock::now();
+ if (last_process_time.time_since_epoch().count() != 0) {
+ const f32 seconds = std::chrono::duration(now - last_process_time).count();
+ const f32 rate = seconds > 0.0f ? 1.0f / seconds : 0.0f;
+ if (rate >= 1.0f && rate <= 1000.0f) {
+ smoothed_base_rate = smoothed_base_rate <= 0.0f
+ ? rate
+ : smoothed_base_rate +
+ (rate - smoothed_base_rate) * BASE_RATE_SMOOTHING;
+ }
+ }
+ last_process_time = now;
+}
+
+size_t FrameGen::DesiredGenerations() const {
+ if (!Settings::values.frame_gen.GetValue()) {
+ return 0;
+ }
+
+ const u32 target = Settings::values.frame_gen_target_rate.GetValue();
+ if (target == 0) {
+ return Settings::FrameGenGenerations();
+ }
+ if (smoothed_base_rate <= 1.0f) {
+ return 0;
+ }
+
+ const size_t multiplier = static_cast(static_cast(target) / smoothed_base_rate);
+ return std::clamp(multiplier, 1, LSFG_MAX_GENERATIONS + 1) - 1;
+}
+
+void FrameGen::UpdateGenerationCount() {
+ const size_t desired = DesiredGenerations();
+ if (desired == stable_generations) {
+ pending_generation_votes = 0;
+ return;
+ }
+
+ if (desired != pending_generations) {
+ pending_generations = desired;
+ pending_generation_votes = 1;
+ return;
+ }
+
+ if (++pending_generation_votes >= GENERATION_CHANGE_VOTES) {
+ stable_generations = desired;
+ pending_generation_votes = 0;
+ }
+}
+
+size_t FrameGen::WantedGenerations() {
+ UpdateBaseRate();
+ UpdateGenerationCount();
+ return unavailable ? 0 : stable_generations;
+}
+
void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, VkExtent2D guest_extent,
bool generate) {
generated = false;
- if (unavailable || ConfiguredGenerations() == 0) {
+ if (unavailable || !Settings::values.frame_gen.GetValue()) {
if (chain) {
scheduler.Finish();
chain.reset();
@@ -234,8 +290,8 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, VkEx
const u64 count = frame_count++;
last_count = count;
- last_generations = ConfiguredGenerations();
- generated = generate && count + 1 >= LSFG_REQUIRED_FRAMES;
+ last_generations = stable_generations;
+ generated = generate && last_generations > 0 && count + 1 >= LSFG_REQUIRED_FRAMES;
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([this, source = *frame->image, extent, count,
@@ -255,13 +311,6 @@ void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, VkEx
}
}
-size_t FrameGen::WantedGenerations() const {
- if (unavailable) {
- return 0;
- }
- return ConfiguredGenerations();
-}
-
size_t FrameGen::GeneratedFrameCount() const {
return generated ? last_generations : 0;
}
diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h
index 5f1e3e4135..66c346503d 100644
--- a/src/video_core/renderer_vulkan/present/frame_gen.h
+++ b/src/video_core/renderer_vulkan/present/frame_gen.h
@@ -3,6 +3,7 @@
#pragma once
+#include
#include
#include "common/common_types.h"
@@ -24,14 +25,19 @@ public:
void Process(const Device& device, Frame* frame, VkFormat format, VkExtent2D guest_extent,
bool generate);
- [[nodiscard]] size_t WantedGenerations() const;
+ [[nodiscard]] size_t WantedGenerations();
[[nodiscard]] size_t GeneratedFrameCount() const;
void GenerateInto(const Device& device, Frame* destination, size_t generation);
private:
+ using Clock = std::chrono::steady_clock;
+
void Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale);
+ void UpdateBaseRate();
+ void UpdateGenerationCount();
+ [[nodiscard]] size_t DesiredGenerations() const;
void DumpDebugImages(u64 count);
MemoryAllocator& memory_allocator;
@@ -46,6 +52,12 @@ private:
u64 frame_count{};
u64 last_count{};
size_t last_generations{};
+
+ Clock::time_point last_process_time{};
+ f32 smoothed_base_rate{};
+ size_t stable_generations{};
+ size_t pending_generations{};
+ u32 pending_generation_votes{};
bool generated{};
bool unavailable{};
bool dumped{};
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp
index 8924241562..0c51fc9e63 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.cpp
+++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp
@@ -351,7 +351,7 @@ 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::FrameGenGenerations();
+ const size_t generations = Settings::FrameGenMaxGenerations();
const size_t queued_composites = Settings::values.frame_gen_queue_target.GetValue() + 1;
image_count =
std::clamp((generations + 1) * queued_composites, swapchain.GetImageCount(),