aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCamilleLaVey <camillelavey99@gmail.com>2026-08-08 04:39:47 +0200
committercrueter <crueter@eden-emu.dev>2026-08-08 04:39:47 +0200
commit3f52bf4b4b34922f9991e4696d27a8f432fe6ea2 (patch)
tree9fee1f6e95de43716b56682e1bcabf1cb90f30b7
parent5ec94b19714f75489cdfc62d890d2f94d45514ce (diff)
[core, gpu, threads] Multithreading refactor (#4254)
This insufferable work tries to cover some holes on previous threading implementation from yuzu's team, starting with Windows and Linux reordering of priorities (NICE), reworks previous Android's threading and cpu affinity with adpf, adjust emulated clocks/gpu for better "accuracy" with their work, bumps android minSDK for all flavors, legacy will now work with AP 29 to cover A10 - A12, standard will reach A13 as base and finally the optimized build will come with API 35, mostly targeted on devices with A15 support and newer, NDK and AGP wasn't upgraded yet. The performance cost efficiency have been improved based on device power configuration; preventing overheating if certain devices tended to fall into NICE0 (not allocated threads priority, all task ran with higher priority, 11 tasks running within the limited 2 - 7 threads available on the most common configuration 1x3x4 or 1x4x3). Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4254
-rw-r--r--src/android/app/build.gradle.kts3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt1
-rw-r--r--src/android/app/src/main/jni/native.cpp5
-rw-r--r--src/android/app/src/main/res/values-ar/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-de/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-es/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-fr/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-pl/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-ru/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-uk/strings.xml1
-rw-r--r--src/android/app/src/main/res/values-zh-rCN/strings.xml1
-rw-r--r--src/android/app/src/main/res/values/arrays.xml4
-rw-r--r--src/android/app/src/main/res/values/strings.xml24
-rw-r--r--src/android/app/src/main/res/xml/game_mode_config.xml2
-rw-r--r--src/common/CMakeLists.txt2
-rw-r--r--src/common/adpf.cpp317
-rw-r--r--src/common/adpf.h26
-rw-r--r--src/common/settings.h20
-rw-r--r--src/common/settings_enums.h4
-rw-r--r--src/common/thread.cpp538
-rw-r--r--src/common/thread.h14
-rw-r--r--src/common/thread_worker.h21
-rw-r--r--src/core/core.cpp2
-rw-r--r--src/core/core_timing.cpp23
-rw-r--r--src/core/cpu_manager.cpp7
-rw-r--r--src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp6
-rw-r--r--src/core/hle/service/nvdrv/devices/nvdisp_disp0.h5
-rw-r--r--src/core/hle/service/nvnflinger/hardware_composer.cpp63
-rw-r--r--src/core/hle/service/nvnflinger/hardware_composer.h3
-rw-r--r--src/core/hle/service/vi/conductor.cpp32
-rw-r--r--src/core/hle/service/vi/conductor.h1
-rw-r--r--src/qt_common/config/shared_translation.cpp29
-rw-r--r--src/video_core/fence_manager.h1
-rw-r--r--src/video_core/gpu.cpp86
-rw-r--r--src/video_core/gpu.h4
-rw-r--r--src/video_core/gpu_thread.cpp1
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp7
-rw-r--r--src/video_core/renderer_vulkan/vk_present_manager.cpp2
-rw-r--r--src/video_core/renderer_vulkan/vk_scheduler.cpp2
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h3
-rw-r--r--src/video_core/textures/workers.cpp3
-rw-r--r--src/yuzu/configuration/configure_cpu.cpp3
45 files changed, 1093 insertions, 187 deletions
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index a4076978a1..0cef7e7196 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -188,6 +188,7 @@ android {
create("mainline") {
dimension = "version"
isDefault = true
+ minSdk = 33
manifestPlaceholders += mapOf("appNameBase" to "Eden")
resValue("string", "app_name_suffixed", "Eden")
@@ -199,6 +200,7 @@ android {
create("genshinSpoof") {
dimension = "version"
+ minSdk = 35
manifestPlaceholders += mapOf("appNameBase" to "Eden Optimized")
resValue("string", "app_name_suffixed", "Eden Optimized")
applicationId = "com.miHoYo.Yuanshen"
@@ -216,6 +218,7 @@ android {
create("legacy") {
dimension = "version"
+ minSdk = 29
manifestPlaceholders += mapOf("appNameBase" to "Eden Legacy")
resValue("string", "app_name_suffixed", "Eden Legacy")
applicationId = "dev.legacy.eden_emulator"
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 a8d6eaf6e1..797ac803eb 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
@@ -218,6 +218,8 @@ object NativeLibrary {
external fun logSettings()
+ external fun refreshThreadPolicies()
+
external fun getDebugKnobAt(index: Int): Boolean
/**
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 7c6063988f..63ac7b0630 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
@@ -594,7 +594,7 @@ abstract class SettingsItem(
IntSetting.ANDROID_PIPELINE_WORKERS,
titleId = R.string.pipeline_worker_cores,
descriptionId = R.string.pipeline_worker_cores_description,
- min = 4,
+ min = 2,
max = 8,
units = "cores"
)
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 81702d7110..594099f159 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
@@ -250,8 +250,9 @@ class SettingsFragmentPresenter(
add(BooleanSetting.USE_CUSTOM_RTC.key)
add(LongSetting.CUSTOM_RTC.key)
- add(HeaderSetting(R.string.cpu))
+ add(HeaderSetting(R.string.clocks))
add(IntSetting.FAST_CPU_TIME.key)
+ add(IntSetting.FAST_GPU_TIME.key)
add(BooleanSetting.CORE_SYNC_CORE_SPEED.key)
add(IntSetting.MEMORY_LAYOUT.key)
@@ -298,7 +299,6 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.hacks))
- add(IntSetting.FAST_GPU_TIME.key)
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
add(BooleanSetting.EMULATE_BGR565.key)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 0069e169b3..5b93c88161 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -1451,6 +1451,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
override fun onResume() {
super.onResume()
+ NativeLibrary.refreshThreadPolicies()
val b = _binding ?: return
updateStatsPosition(IntSetting.PERF_OVERLAY_POSITION.getInt())
updateSocPosition(IntSetting.SOC_OVERLAY_POSITION.getInt())
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 41ba1b2019..c367a06dfd 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -50,6 +50,7 @@ extern "C" {
#include "common/scope_exit.h"
#include "common/settings.h"
#include "common/string_util.h"
+#include "common/thread.h"
#include "frontend_common/play_time_manager.h"
#include "core/constants.h"
#include "core/core.h"
@@ -1182,6 +1183,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj
Settings::LogSettings();
}
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_refreshThreadPolicies(JNIEnv* env, jobject jobj) {
+ Common::RefreshThreadPolicies();
+}
+
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobject jobj, jint index) {
return static_cast<jboolean>(Settings::getDebugKnobAt(static_cast<u8>(index)));
}
diff --git a/src/android/app/src/main/res/values-ar/strings.xml b/src/android/app/src/main/res/values-ar/strings.xml
index 616f160fd1..dd03ec453f 100644
--- a/src/android/app/src/main/res/values-ar/strings.xml
+++ b/src/android/app/src/main/res/values-ar/strings.xml
@@ -958,7 +958,6 @@
<string name="clock_fast">سريع (2000MHz)</string>
<!-- GPU overclock factors -->
- <string name="off">تعطيل</string>
<string name="fast_gpu_medium">متوسط (256)</string>
<string name="fast_gpu_high">مرتفع (512)</string>
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml
index 182247580c..1c3b6c148f 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -884,7 +884,6 @@ Wirklich fortfahren?</string>
<string name="clock_fast">Schnell (2000MHz)</string>
<!-- GPU overclock factors -->
- <string name="off">Aus</string>
<string name="fast_gpu_medium">Mittel (256)</string>
<string name="fast_gpu_high">Hoch (512)</string>
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml
index 37bd14b2d1..a5092c4b2f 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -950,7 +950,6 @@
<string name="clock_fast">Rápido (2000MHz)</string>
<!-- GPU overclock factors -->
- <string name="off">Desactivado</string>
<string name="fast_gpu_medium">Medio (256)</string>
<string name="fast_gpu_high">Alto (512)</string>
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml
index 2cf6a1da91..602a542c5f 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -897,7 +897,6 @@
<string name="clock_fast">Rapide (2000MHz)</string>
<!-- GPU overclock factors -->
- <string name="off">Désactivé</string>
<string name="fast_gpu_medium">Moyen (256)</string>
<string name="fast_gpu_high">Élevé (512)</string>
diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml
index d96d420a98..4bcebd5a8c 100644
--- a/src/android/app/src/main/res/values-pl/strings.xml
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -871,7 +871,6 @@
<string name="clock_fast">Szybkie (2000MHz)</string>
<!-- GPU overclock factors -->
- <string name="off">Wyłączone</string>
<string name="fast_gpu_medium">Średnie (256)</string>
<string name="fast_gpu_high">Wysokie (512)</string>
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml
index 6e85084b77..3dbeb6ffdb 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -954,7 +954,6 @@
<string name="clock_fast">Быстрая (2000MHz)</string>
<!-- GPU overclock factors -->
- <string name="off">Выкл.</string>
<string name="fast_gpu_medium">Среднее (256)</string>
<string name="fast_gpu_high">Высокое (512)</string>
diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml
index 86ea6ba787..83893210bd 100644
--- a/src/android/app/src/main/res/values-uk/strings.xml
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -943,7 +943,6 @@
<string name="clock_fast">Швидко (2000 МГц)</string>
<!-- GPU overclock factors -->
- <string name="off">Вимкнено</string>
<string name="fast_gpu_medium">Середньо (256)</string>
<string name="fast_gpu_high">Високо (512)</string>
diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml
index ff2db086e3..1e6ba26041 100644
--- a/src/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -948,7 +948,6 @@
<string name="clock_fast">快速 (2000MHz)</string>
<!-- GPU overclock factors -->
- <string name="off">关闭</string>
<string name="fast_gpu_medium">中 (256)</string>
<string name="fast_gpu_high">高 (512)</string>
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 7a69d5b48b..1bad110d14 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -34,7 +34,7 @@
</integer-array>
<string-array name="clockNames">
- <item>@string/off</item>
+ <item>@string/clock_normal</item>
<item>@string/clock_boost</item>
<item>@string/clock_fast</item>
</string-array>
@@ -547,7 +547,7 @@
</integer-array>
<string-array name="gpuEntries">
- <item>@string/off</item>
+ <item>@string/fast_gpu_normal</item>
<item>@string/fast_gpu_medium</item>
<item>@string/fast_gpu_high</item>
</string-array>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 3023327f14..bb1d85db82 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -450,8 +450,8 @@
<string name="set_custom_rtc">Set custom RTC</string>
<!-- CPU -->
- <string name="fast_cpu_time">CPU Overclock</string>
- <string name="fast_cpu_time_description">Forces the emulated CPU to run at a higher clock, reducing certain FPS limiters. Use Boost (1700MHz) to run at the Switch\'s highest native clock, or Fast (2000MHz) to run at 2x clock.</string>
+ <string name="fast_cpu_time">CPU Clocks</string>
+ <string name="fast_cpu_time_description">Raises the clock the emulated CPU reports, which removes some FPS limiters. Weaker CPUs may see reduced performance, and certain games may behave improperly.</string>
<string name="custom_cpu_ticks">Custom CPU Ticks</string>
<string name="custom_cpu_ticks_description">Set a custom value of CPU ticks. Higher values can increase performance, but may also cause the game to freeze. A range of 77–21000 is recommended.</string>
<string name="cpu_ticks">Ticks</string>
@@ -512,8 +512,8 @@
<string name="hacks">Hacks</string>
- <string name="fast_gpu_time">Fast GPU Time</string>
- <string name="fast_gpu_time_description">Forces most games to run at their highest native resolution. Use 256 for maximal performance and 512 for maximal graphics fidelity.</string>
+ <string name="fast_gpu_time">GPU Clocks</string>
+ <string name="fast_gpu_time_description">Makes the game believe GPU work finishes faster than it does, so it stops lowering resolution and render distance to fit the Switch\'s clocks.</string>
<string name="skip_cpu_inner_invalidation">Skip CPU Inner Invalidation</string>
<string name="skip_cpu_inner_invalidation_description">Skips certain CPU-side cache invalidations during memory updates, reducing CPU usage and improving it\'s performance. This may cause glitches or crashes on some games.</string>
<string name="fix_bloom_effects">Fix Bloom Effects</string>
@@ -560,6 +560,7 @@
<!-- Debug settings strings -->
<string name="cpu">CPU</string>
+ <string name="clocks">Clocks</string>
<string name="use_auto_stub">Use Auto Stub</string>
<string name="use_auto_stub_description">Automatically stub missing services and functions. This may improve compatibility but can cause crashes and stability issues.</string>
@@ -968,14 +969,15 @@
<string name="memory_6gb">6GB (Unsafe)</string>
<string name="memory_8gb">8GB (Unsafe)</string>
- <!--CPU clock speeds-->
- <string name="clock_boost">Boost (1700MHz)</string>
- <string name="clock_fast">Fast (2000MHz)</string>
+ <!-- CPU clock levels -->
+ <string name="clock_normal">Normal</string>
+ <string name="clock_boost">Boost</string>
+ <string name="clock_fast">Overclock</string>
- <!-- GPU overclock factors -->
- <string name="off">Off</string>
- <string name="fast_gpu_medium">Medium (256)</string>
- <string name="fast_gpu_high">High (512)</string>
+ <!-- GPU clock levels -->
+ <string name="fast_gpu_normal">Normal</string>
+ <string name="fast_gpu_medium">Boost</string>
+ <string name="fast_gpu_high">Overclock</string>
<!-- GPU swizzle texture size -->
<string name="gpu_texturesizeswizzle_verysmall">Very Small (16 MB)</string>
diff --git a/src/android/app/src/main/res/xml/game_mode_config.xml b/src/android/app/src/main/res/xml/game_mode_config.xml
index 8f6e90d0d2..69e038cb5e 100644
--- a/src/android/app/src/main/res/xml/game_mode_config.xml
+++ b/src/android/app/src/main/res/xml/game_mode_config.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<game-mode-config
xmlns:android="http://schemas.android.com/apk/res/android"
- android:supportsBatteryGameMode="true"
+ android:supportsBatteryGameMode="false"
android:supportsPerformanceGameMode="true"
android:allowGameDownscaling="false"
android:allowGameFpsOverride="false"/> \ No newline at end of file
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 87c4642f04..923d001442 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -121,6 +121,8 @@ add_library(
swap.h
thread.cpp
thread.h
+ adpf.cpp
+ adpf.h
thread_queue_list.h
thread_worker.h
threadsafe_queue.h
diff --git a/src/common/adpf.cpp b/src/common/adpf.cpp
new file mode 100644
index 0000000000..8fa2996173
--- /dev/null
+++ b/src/common/adpf.cpp
@@ -0,0 +1,317 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "common/adpf.h"
+
+#ifdef __ANDROID__
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <cstdint>
+#include <mutex>
+#include <vector>
+
+#include <dlfcn.h>
+#include <unistd.h>
+
+#include "common/logging.h"
+
+namespace Common::ADPF {
+
+namespace {
+
+constexpr std::chrono::nanoseconds DEFAULT_TARGET = std::chrono::nanoseconds{16'666'667};
+
+struct AHintManager;
+struct AHintSession;
+
+using PFN_GetManager = AHintManager* (*)();
+using PFN_CreateSession = AHintSession* (*)(AHintManager*, const s32*, size_t, s64);
+using PFN_CloseSession = void (*)(AHintSession*);
+using PFN_UpdateTarget = int (*)(AHintSession*, s64);
+using PFN_ReportActual = int (*)(AHintSession*, s64);
+using PFN_SetThreads = int (*)(AHintSession*, const pid_t*, size_t);
+using PFN_SetPowerEfficiency = int (*)(AHintSession*, bool);
+
+struct Api {
+ PFN_GetManager get_manager = nullptr;
+ PFN_CreateSession create_session = nullptr;
+ PFN_CloseSession close_session = nullptr;
+ PFN_UpdateTarget update_target = nullptr;
+ PFN_ReportActual report_actual = nullptr;
+ PFN_SetThreads set_threads = nullptr;
+ PFN_SetPowerEfficiency set_power_efficiency = nullptr;
+ AHintManager* manager = nullptr;
+ bool usable = false;
+};
+
+const Api& Resolve() {
+ static const Api api = [] {
+ Api resolved;
+ void* library = dlopen("libandroid.so", RTLD_NOW);
+ if (library == nullptr) {
+ LOG_INFO(Common, "libandroid.so unavailable, ADPF is disabled");
+ return resolved;
+ }
+ const auto load = [library](const char* name) { return dlsym(library, name); };
+
+ resolved.get_manager = reinterpret_cast<PFN_GetManager>(load("APerformanceHint_getManager"));
+ resolved.create_session =
+ reinterpret_cast<PFN_CreateSession>(load("APerformanceHint_createSession"));
+ resolved.close_session =
+ reinterpret_cast<PFN_CloseSession>(load("APerformanceHint_closeSession"));
+ resolved.update_target =
+ reinterpret_cast<PFN_UpdateTarget>(load("APerformanceHint_updateTargetWorkDuration"));
+ resolved.report_actual =
+ reinterpret_cast<PFN_ReportActual>(load("APerformanceHint_reportActualWorkDuration"));
+ resolved.set_threads =
+ reinterpret_cast<PFN_SetThreads>(load("APerformanceHint_setThreads"));
+ resolved.set_power_efficiency = reinterpret_cast<PFN_SetPowerEfficiency>(
+ load("APerformanceHint_setPreferPowerEfficiency"));
+
+ if (resolved.get_manager == nullptr || resolved.create_session == nullptr ||
+ resolved.close_session == nullptr || resolved.update_target == nullptr ||
+ resolved.report_actual == nullptr) {
+ LOG_INFO(Common, "Performance hint API not exported, ADPF is disabled");
+ return resolved;
+ }
+
+ resolved.manager = resolved.get_manager();
+ if (resolved.manager == nullptr) {
+ LOG_INFO(Common, "Device does not provide a performance hint manager");
+ return resolved;
+ }
+
+ resolved.usable = true;
+ LOG_INFO(Common, "ADPF available, setThreads {}, power efficiency {}",
+ resolved.set_threads != nullptr ? "yes" : "no",
+ resolved.set_power_efficiency != nullptr ? "yes" : "no");
+ return resolved;
+ }();
+ return api;
+}
+
+struct SessionState {
+ AHintSession* handle = nullptr;
+ std::vector<pid_t> threads;
+ bool unsupported = false;
+};
+
+std::mutex g_mutex;
+std::array<SessionState, 2> g_sessions;
+
+constexpr s64 MAX_REPORTED_TARGETS = 4;
+
+std::atomic<s64> g_target_ns{DEFAULT_TARGET.count()};
+thread_local std::chrono::steady_clock::time_point t_last_frame{};
+
+SessionState& StateOf(Session session) {
+ return g_sessions[static_cast<size_t>(session)];
+}
+
+bool IsBackgroundUsable(const Api& api) {
+ return api.set_power_efficiency != nullptr;
+}
+
+void CloseLocked(SessionState& state) {
+ if (state.handle != nullptr) {
+ Resolve().close_session(state.handle);
+ state.handle = nullptr;
+ }
+}
+
+AHintSession* CreateSessionFor(Session session, const std::vector<pid_t>& threads) {
+ const Api& api = Resolve();
+ const s64 target = session == Session::Render ? g_target_ns.load(std::memory_order_relaxed) : 0;
+
+ std::vector<s32> ids;
+ ids.reserve(threads.size());
+ for (const pid_t tid : threads) {
+ ids.push_back(static_cast<s32>(tid));
+ }
+
+ AHintSession* handle = api.create_session(api.manager, ids.data(), ids.size(), target);
+ if (handle == nullptr && target == 0) {
+ handle = api.create_session(api.manager, ids.data(), ids.size(), DEFAULT_TARGET.count());
+ }
+ if (handle == nullptr) {
+ return nullptr;
+ }
+ if (session == Session::Background && api.set_power_efficiency != nullptr) {
+ api.set_power_efficiency(handle, true);
+ }
+ return handle;
+}
+
+bool SyncLocked(Session session, SessionState& state) {
+ if (state.threads.empty()) {
+ CloseLocked(state);
+ return false;
+ }
+
+ const Api& api = Resolve();
+ if (state.handle != nullptr && api.set_threads != nullptr) {
+ std::vector<pid_t> ids = state.threads;
+ if (api.set_threads(state.handle, ids.data(), ids.size()) == 0) {
+ return true;
+ }
+ }
+
+ AHintSession* const replacement = CreateSessionFor(session, state.threads);
+ if (replacement == nullptr) {
+ if (state.handle == nullptr) {
+ state.unsupported = true;
+ }
+ LOG_WARNING(Common, "Could not open a performance hint session for {} threads, falling back",
+ state.threads.size());
+ return false;
+ }
+ CloseLocked(state);
+ state.handle = replacement;
+ return true;
+}
+
+} // Anonymous namespace
+
+bool IsSessionSupported(Session session) {
+ const Api& api = Resolve();
+ if (!api.usable) {
+ return false;
+ }
+ if (session == Session::Background && !IsBackgroundUsable(api)) {
+ return false;
+ }
+ std::scoped_lock lock{g_mutex};
+ return !StateOf(session).unsupported;
+}
+
+bool AddCurrentThread(Session session) {
+ if (!IsSessionSupported(session)) {
+ return false;
+ }
+
+ const pid_t tid = gettid();
+ std::scoped_lock lock{g_mutex};
+
+ for (size_t i = 0; i < g_sessions.size(); ++i) {
+ SessionState& state = g_sessions[i];
+ if (static_cast<size_t>(session) == i) {
+ continue;
+ }
+ const auto it = std::find(state.threads.begin(), state.threads.end(), tid);
+ if (it != state.threads.end()) {
+ state.threads.erase(it);
+ SyncLocked(static_cast<Session>(i), state);
+ }
+ }
+
+ SessionState& state = StateOf(session);
+ const bool added =
+ std::find(state.threads.begin(), state.threads.end(), tid) == state.threads.end();
+ if (added) {
+ state.threads.push_back(tid);
+ }
+ if (!SyncLocked(session, state)) {
+ if (added) {
+ std::erase(state.threads, tid);
+ }
+ return false;
+ }
+ return true;
+}
+
+void RemoveCurrentThread() {
+ if (!Resolve().usable) {
+ return;
+ }
+ const pid_t tid = gettid();
+ std::scoped_lock lock{g_mutex};
+ for (size_t i = 0; i < g_sessions.size(); ++i) {
+ SessionState& state = g_sessions[i];
+ if (std::erase(state.threads, tid) != 0) {
+ SyncLocked(static_cast<Session>(i), state);
+ }
+ }
+}
+
+void SetTargetWorkDuration(std::chrono::nanoseconds target) {
+ const Api& api = Resolve();
+ if (!api.usable || target.count() <= 0) {
+ return;
+ }
+ if (g_target_ns.exchange(target.count(), std::memory_order_relaxed) == target.count()) {
+ return;
+ }
+ std::scoped_lock lock{g_mutex};
+ SessionState& state = StateOf(Session::Render);
+ if (state.handle != nullptr) {
+ api.update_target(state.handle, target.count());
+ }
+}
+
+void ReportFrameInterval() {
+ const Api& api = Resolve();
+ if (!api.usable) {
+ return;
+ }
+
+ const auto now = std::chrono::steady_clock::now();
+ const auto previous = t_last_frame;
+ t_last_frame = now;
+ if (previous.time_since_epoch().count() == 0) {
+ return;
+ }
+
+ s64 actual = std::chrono::duration_cast<std::chrono::nanoseconds>(now - previous).count();
+ if (actual <= 0) {
+ return;
+ }
+
+ const s64 ceiling = g_target_ns.load(std::memory_order_relaxed) * MAX_REPORTED_TARGETS;
+ actual = (std::min)(actual, ceiling);
+
+ std::scoped_lock lock{g_mutex};
+ SessionState& state = StateOf(Session::Render);
+ if (state.handle != nullptr) {
+ api.report_actual(state.handle, actual);
+ }
+}
+
+void Shutdown() {
+ if (!Resolve().usable) {
+ return;
+ }
+ std::scoped_lock lock{g_mutex};
+ for (SessionState& state : g_sessions) {
+ CloseLocked(state);
+ state.threads.clear();
+ state.unsupported = false;
+ }
+}
+
+} // namespace Common::ADPF
+
+#else
+
+namespace Common::ADPF {
+
+bool IsSessionSupported(Session) {
+ return false;
+}
+
+bool AddCurrentThread(Session) {
+ return false;
+}
+
+void RemoveCurrentThread() {}
+
+void SetTargetWorkDuration(std::chrono::nanoseconds) {}
+
+void ReportFrameInterval() {}
+
+void Shutdown() {}
+
+} // namespace Common::ADPF
+
+#endif
diff --git a/src/common/adpf.h b/src/common/adpf.h
new file mode 100644
index 0000000000..6f14a0bee4
--- /dev/null
+++ b/src/common/adpf.h
@@ -0,0 +1,26 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <chrono>
+
+namespace Common::ADPF {
+
+enum class Session {
+ Render,
+ Background,
+};
+
+bool IsSessionSupported(Session session);
+
+bool AddCurrentThread(Session session);
+void RemoveCurrentThread();
+
+void SetTargetWorkDuration(std::chrono::nanoseconds target);
+
+void ReportFrameInterval();
+
+void Shutdown();
+
+} // namespace Common::ADPF
diff --git a/src/common/settings.h b/src/common/settings.h
index 00ba7dddc9..d25d5f690b 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -260,10 +260,10 @@ struct Values {
Category::Cpu};
SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage, CpuAccuracy::Auto,
"cpu_accuracy", Category::Cpu};
- SwitchableSetting<CpuClock> fast_cpu_time{linkage,
- CpuClock::Off,
+ SwitchableSetting<CpuClock> cpu_clock{linkage,
+ CpuClock::Normal,
"fast_cpu_time",
- Category::Cpu,
+ Category::System,
Specialization::Default,
true,
true};
@@ -540,13 +540,13 @@ struct Values {
#endif
// Renderer Hacks //
- SwitchableSetting<GpuOverclock> fast_gpu_time{linkage,
- GpuOverclock::Medium,
- "fast_gpu_time",
- Category::RendererHacks,
- Specialization::Default,
- true,
- true};
+ SwitchableSetting<GpuClock> gpu_clock{linkage,
+ GpuClock::Boost,
+ "fast_gpu_time",
+ Category::System,
+ Specialization::Default,
+ true,
+ true};
SwitchableSetting<bool> skip_cpu_inner_invalidation{linkage,
false,
diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h
index 2db0e02a5f..3d0131de70 100644
--- a/src/common/settings_enums.h
+++ b/src/common/settings_enums.h
@@ -140,7 +140,7 @@ ENUM(DmaAccuracy, Default, Unsafe, Safe);
ENUM(GpuFenceBehavior, Default, Immediate, Balanced, Accurate, Strict);
ENUM(CpuBackend, Dynarmic, Nce);
ENUM(CpuAccuracy, Auto, Accurate, Unsafe, Paranoid, Debugging);
-ENUM(CpuClock, Off, Boost, Fast)
+ENUM(CpuClock, Normal, Boost, Overclock)
ENUM(MemoryLayout, Memory_4Gb, Memory_6Gb, Memory_8Gb, Memory_10Gb, Memory_12Gb);
ENUM(ConfirmStop, Ask_Always, Ask_Based_On_Game, Ask_Never);
ENUM(FullscreenMode, Borderless, Exclusive);
@@ -152,7 +152,7 @@ ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch);
ENUM(ConsoleMode, Handheld, Docked);
ENUM(AppletMode, HLE, LLE);
ENUM(SpirvOptimizeMode, Never, OnLoad, Always);
-ENUM(GpuOverclock, Normal, Medium, High)
+ENUM(GpuClock, Normal, Boost, Overclock)
ENUM(GpuUnswizzleSize, VerySmall, Small, Normal, Large, VeryLarge)
ENUM(GpuUnswizzle, VeryLow, Low, Normal, Medium, High)
ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High)
diff --git a/src/common/thread.cpp b/src/common/thread.cpp
index c947c88373..7b4dec3dce 100644
--- a/src/common/thread.cpp
+++ b/src/common/thread.cpp
@@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -9,6 +10,7 @@
#include <string>
#include <thread>
+#include "common/adpf.h"
#include "common/error.h"
#include "common/logging.h"
#include "common/assert.h"
@@ -39,6 +41,402 @@
#include <unistd.h>
#endif
+#ifdef __linux__
+#include <sys/resource.h>
+#include <algorithm>
+
+namespace {
+constexpr int NICE_AUDIO = -16;
+constexpr int NICE_URGENT_DISPLAY = -8;
+constexpr int NICE_DISPLAY = -4;
+constexpr int NICE_DEFAULT = 0;
+constexpr int NICE_BACKGROUND = 10;
+
+int LowestAllowedNice() {
+ static const int lowest = [] {
+ rlimit limit{};
+ if (getrlimit(RLIMIT_NICE, &limit) != 0) {
+ return 0;
+ }
+ if (limit.rlim_cur >= 40) {
+ return -20;
+ }
+ return 20 - static_cast<int>(limit.rlim_cur);
+ }();
+ return lowest;
+}
+
+int NiceValueForPriority(Common::ThreadPriority priority) {
+ const int wanted = [priority] {
+ switch (priority) {
+ case Common::ThreadPriority::Low: return NICE_BACKGROUND;
+ case Common::ThreadPriority::Normal: return NICE_DEFAULT;
+ case Common::ThreadPriority::High: return NICE_DISPLAY;
+ case Common::ThreadPriority::VeryHigh: return NICE_URGENT_DISPLAY;
+ case Common::ThreadPriority::Critical: return NICE_AUDIO;
+ default: return NICE_DEFAULT;
+ }
+ }();
+ return (std::max)(wanted, (std::min)(NICE_DEFAULT, LowestAllowedNice()));
+}
+} // Anonymous namespace
+#endif
+
+#ifdef __ANDROID__
+#include <sys/utsname.h>
+#include <atomic>
+#include <cerrno>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <mutex>
+#include <vector>
+
+namespace {
+constexpr size_t ANDROID_MINIMUM_PERFORMANCE_CORES = 4;
+
+// A core counts as a performance core while it is within this much of the fastest one.
+constexpr s64 ANDROID_PERFORMANCE_CAPACITY_PERCENT = 50;
+
+constexpr std::chrono::nanoseconds ANDROID_POLICY_POLL_INTERVAL = std::chrono::milliseconds{500};
+
+enum class CoreGroup {
+ Unrestricted,
+ Performance,
+ Efficiency,
+};
+
+struct ThreadPolicy {
+ pid_t tid;
+ CoreGroup group;
+ s32 nice_value;
+ bool has_nice;
+};
+
+struct CoreInfo {
+ s64 weight;
+ u64 midr;
+ s32 cpu;
+};
+
+struct CpuTopologyState {
+ std::mutex topology_mutex;
+ cpu_set_t allowed{};
+ cpu_set_t performance{};
+ cpu_set_t efficiency{};
+ bool separated = false;
+ bool initialized = false;
+
+ pid_t canary_tid = 0;
+ cpu_set_t canary_mask{};
+ bool canary_valid = false;
+
+ std::atomic<s64> next_poll_ns{0};
+
+ std::mutex policy_mutex;
+ std::vector<ThreadPolicy> policies;
+};
+
+CpuTopologyState& State() {
+ static CpuTopologyState* const state = new CpuTopologyState();
+ return *state;
+}
+
+struct PolicyRegistration {
+ ~PolicyRegistration() {
+ const pid_t tid = gettid();
+ ::Common::ADPF::RemoveCurrentThread();
+ CpuTopologyState& state = State();
+ std::scoped_lock lock{state.policy_mutex};
+ std::erase_if(state.policies,
+ [tid](const ThreadPolicy& policy) { return policy.tid == tid; });
+ }
+};
+
+thread_local PolicyRegistration t_policy_registration;
+
+s32 PossibleCpuCount() {
+ std::ifstream file("/sys/devices/system/cpu/possible");
+ std::string list;
+ if (file && std::getline(file, list) && !list.empty()) {
+ s64 highest = -1;
+ const char* cursor = list.c_str();
+ while (*cursor != '\0') {
+ char* end = nullptr;
+ const s64 value = std::strtol(cursor, &end, 10);
+ if (end == cursor) {
+ break;
+ }
+ highest = (std::max)(highest, value);
+ cursor = end;
+ while (*cursor == '-' || *cursor == ',') {
+ ++cursor;
+ }
+ }
+ if (highest >= 0) {
+ return static_cast<s32>((std::min<s64>)(highest + 1, CPU_SETSIZE));
+ }
+ }
+ const s64 configured = sysconf(_SC_NPROCESSORS_CONF);
+ if (configured > 0) {
+ return static_cast<s32>((std::min<s64>)(configured, CPU_SETSIZE));
+ }
+ return static_cast<s32>((std::min<s64>)(std::thread::hardware_concurrency(), CPU_SETSIZE));
+}
+
+s64 ReadCpuScalar(s32 cpu, const char* node) {
+ s64 value = 0;
+ std::ifstream file("/sys/devices/system/cpu/cpu" + std::to_string(cpu) + "/" + node);
+ if (!file || !(file >> value) || value <= 0) {
+ return 0;
+ }
+ return value;
+}
+
+u64 ReadCpuMidr(s32 cpu) {
+ u64 midr = 0;
+ std::ifstream file("/sys/devices/system/cpu/cpu" + std::to_string(cpu) +
+ "/regs/identification/midr_el1");
+ if (!file || !(file >> std::hex >> midr)) {
+ return 0;
+ }
+ return midr;
+}
+
+std::vector<CoreInfo> CollectCores(const cpu_set_t& allowed, s32 total, const char* node,
+ bool require_all) {
+ std::vector<CoreInfo> cores;
+ for (s32 cpu = 0; cpu < total; ++cpu) {
+ if (!CPU_ISSET(cpu, &allowed)) {
+ continue;
+ }
+ const s64 weight = ReadCpuScalar(cpu, node);
+ if (weight <= 0) {
+ if (require_all) {
+ return {};
+ }
+ LOG_WARNING(Common, "Could not read {} for CPU {}, treating it as an efficiency core",
+ node, cpu);
+ continue;
+ }
+ cores.push_back(CoreInfo{weight, ReadCpuMidr(cpu), cpu});
+ }
+ return cores;
+}
+
+bool WeightsAreUniform(const std::vector<CoreInfo>& cores) {
+ return std::all_of(cores.begin(), cores.end(),
+ [&](const CoreInfo& core) { return core.weight == cores.front().weight; });
+}
+
+bool MidrsAreDistinct(const std::vector<CoreInfo>& cores) {
+ return std::none_of(cores.begin(), cores.end(),
+ [](const CoreInfo& core) { return core.midr == 0; }) &&
+ std::any_of(cores.begin(), cores.end(),
+ [&](const CoreInfo& core) { return core.midr != cores.front().midr; });
+}
+
+void ComputeTopologyLocked(CpuTopologyState& state) {
+ state.initialized = true;
+ state.separated = false;
+ CPU_ZERO(&state.allowed);
+ CPU_ZERO(&state.performance);
+ CPU_ZERO(&state.efficiency);
+
+ if (sched_getaffinity(getpid(), sizeof(state.allowed), &state.allowed) != 0) {
+ LOG_WARNING(Common, "Could not query process CPU affinity: {}",
+ ::Common::GetLastErrorMsg());
+ return;
+ }
+
+ const s32 total = PossibleCpuCount();
+ auto cores = CollectCores(state.allowed, total, "cpu_capacity", true);
+ if (cores.empty() || (WeightsAreUniform(cores) && MidrsAreDistinct(cores))) {
+ auto by_frequency = CollectCores(state.allowed, total, "cpufreq/cpuinfo_max_freq", false);
+ if (!by_frequency.empty()) {
+ cores = std::move(by_frequency);
+ }
+ }
+ if (cores.empty()) {
+ LOG_WARNING(Common, "Could not determine CPU topology, thread placement is disabled");
+ return;
+ }
+
+ if (WeightsAreUniform(cores)) {
+ if (MidrsAreDistinct(cores)) {
+ LOG_WARNING(Common, "CPU clusters differ but rank identically, thread placement is "
+ "disabled");
+ } else {
+ LOG_INFO(Common, "CPU cores are symmetric, thread placement is disabled");
+ }
+ return;
+ }
+
+ std::sort(cores.begin(), cores.end(), [](const CoreInfo& lhs, const CoreInfo& rhs) {
+ if (lhs.weight != rhs.weight) {
+ return lhs.weight > rhs.weight;
+ }
+ return lhs.cpu < rhs.cpu;
+ });
+
+ const s64 fastest = cores.front().weight;
+ size_t taken = 0;
+ for (const auto& core : cores) {
+ const bool fast_enough =
+ core.weight * 100 >= fastest * ANDROID_PERFORMANCE_CAPACITY_PERCENT;
+ if (!fast_enough && taken >= ANDROID_MINIMUM_PERFORMANCE_CORES) {
+ break;
+ }
+ CPU_SET(core.cpu, &state.performance);
+ ++taken;
+ }
+ if (taken == 0) {
+ return;
+ }
+
+ for (s32 cpu = 0; cpu < total; ++cpu) {
+ if (CPU_ISSET(cpu, &state.allowed) && !CPU_ISSET(cpu, &state.performance)) {
+ CPU_SET(cpu, &state.efficiency);
+ }
+ }
+
+ state.separated = CPU_COUNT(&state.efficiency) > 0;
+ LOG_INFO(Common, "CPU topology: {} performance cores, {} efficiency cores, separation {}",
+ CPU_COUNT(&state.performance), CPU_COUNT(&state.efficiency),
+ state.separated ? "enabled" : "unavailable");
+}
+
+void EnsureTopologyLocked(CpuTopologyState& state) {
+ if (!state.initialized) {
+ ComputeTopologyLocked(state);
+ }
+}
+
+void RefreshTopologyLocked(CpuTopologyState& state) {
+ if (!state.initialized) {
+ ComputeTopologyLocked(state);
+ return;
+ }
+ cpu_set_t current;
+ CPU_ZERO(&current);
+ if (sched_getaffinity(getpid(), sizeof(current), &current) != 0) {
+ return;
+ }
+ if (std::memcmp(&current, &state.allowed, sizeof(current)) != 0) {
+ ComputeTopologyLocked(state);
+ }
+}
+
+bool ApplyCoreGroupLocked(CpuTopologyState& state, pid_t tid, CoreGroup group,
+ bool* gone = nullptr) {
+ const bool restrict_group = group != CoreGroup::Unrestricted && state.separated;
+ const cpu_set_t* mask = &state.allowed;
+ if (restrict_group) {
+ mask = group == CoreGroup::Performance ? &state.performance : &state.efficiency;
+ }
+ if (CPU_COUNT(mask) == 0) {
+ return false;
+ }
+ if (sched_setaffinity(tid, sizeof(*mask), mask) != 0) {
+ if (gone != nullptr && errno == ESRCH) {
+ *gone = true;
+ return false;
+ }
+ LOG_WARNING(Common, "Could not restrict thread {} to its core group: {}", tid,
+ ::Common::GetLastErrorMsg());
+ return false;
+ }
+ return true;
+}
+
+bool KernelPreservesRequestedAffinity() {
+ utsname info{};
+ if (uname(&info) != 0) {
+ return false;
+ }
+ s32 major = 0;
+ s32 minor = 0;
+ if (std::sscanf(info.release, "%d.%d", &major, &minor) != 2) {
+ return false;
+ }
+ return major > 6 || (major == 6 && minor >= 2);
+}
+
+void SnapshotCanaryLocked(CpuTopologyState& state) {
+ state.canary_valid = false;
+ if (!state.separated) {
+ return;
+ }
+ for (const auto& policy : state.policies) {
+ if (policy.group == CoreGroup::Unrestricted) {
+ continue;
+ }
+ cpu_set_t mask;
+ CPU_ZERO(&mask);
+ if (sched_getaffinity(policy.tid, sizeof(mask), &mask) != 0) {
+ continue;
+ }
+ state.canary_tid = policy.tid;
+ state.canary_mask = mask;
+ state.canary_valid = true;
+ return;
+ }
+}
+
+bool DueForPoll(CpuTopologyState& state) {
+ const auto now = std::chrono::steady_clock::now().time_since_epoch();
+ const s64 now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now).count();
+ s64 next = state.next_poll_ns.load(std::memory_order_relaxed);
+ if (now_ns < next) {
+ return false;
+ }
+ return state.next_poll_ns.compare_exchange_strong(
+ next, now_ns + ANDROID_POLICY_POLL_INTERVAL.count(), std::memory_order_relaxed);
+}
+
+ThreadPolicy& AcquirePolicyLocked(CpuTopologyState& state, pid_t tid) {
+ for (auto& policy : state.policies) {
+ if (policy.tid == tid) {
+ return policy;
+ }
+ }
+ return state.policies.emplace_back(ThreadPolicy{tid, CoreGroup::Unrestricted, 0, false});
+}
+
+void SetCurrentThreadCoreGroup(CoreGroup group) {
+ const pid_t tid = gettid();
+ (void)&t_policy_registration;
+
+ CoreGroup effective = group;
+ if (tid == getpid() && group != CoreGroup::Unrestricted) {
+ LOG_WARNING(Common, "Refusing to place the main thread: the CPU topology is read from it");
+ effective = CoreGroup::Unrestricted;
+ }
+
+ CpuTopologyState& state = State();
+ std::scoped_lock topology_lock{state.topology_mutex};
+ EnsureTopologyLocked(state);
+ ApplyCoreGroupLocked(state, tid, effective);
+
+ std::scoped_lock policy_lock{state.policy_mutex};
+ AcquirePolicyLocked(state, tid).group = effective;
+ if (!state.canary_valid) {
+ SnapshotCanaryLocked(state);
+ }
+}
+
+void RememberCurrentThreadNice(pid_t tid, s32 nice_value) {
+ (void)&t_policy_registration;
+ CpuTopologyState& state = State();
+ std::scoped_lock lock{state.policy_mutex};
+ ThreadPolicy& policy = AcquirePolicyLocked(state, tid);
+ policy.nice_value = nice_value;
+ policy.has_nice = true;
+}
+} // Anonymous namespace
+#endif
+
#include "common/cpu_features.h"
#ifdef ARCHITECTURE_x86_64
#ifdef _MSC_VER
@@ -48,7 +446,6 @@
#endif
#include "common/x64/rdtsc.h"
#endif
-#include "core/core_timing.h"
namespace Common {
@@ -78,21 +475,31 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) {
}
}();
set_thread_priority(find_thread(NULL), priority);
+#elif defined(__ANDROID__)
+ const int nice_value = NiceValueForPriority(new_priority);
+ const pid_t tid = gettid();
+ if (setpriority(PRIO_PROCESS, static_cast<id_t>(tid), nice_value) != 0) {
+ LOG_WARNING(Common, "Could not set thread nice value to {}: {}", nice_value,
+ GetLastErrorMsg());
+ return;
+ }
+ RememberCurrentThreadNice(tid, nice_value);
+#elif defined(__linux__)
+ const int nice_value = NiceValueForPriority(new_priority);
+ if (setpriority(PRIO_PROCESS, 0, nice_value) != 0) {
+ LOG_DEBUG(Common, "Could not set thread nice value to {}: {}", nice_value,
+ GetLastErrorMsg());
+ }
#else
- pthread_t this_thread = pthread_self();
- const auto scheduling_type = SCHED_OTHER;
- s32 max_prio = sched_get_priority_max(scheduling_type);
- s32 min_prio = sched_get_priority_min(scheduling_type);
- u32 level = (std::max)(u32(new_priority) + 1, 4U);
-
- struct sched_param params;
+ const s32 max_prio = sched_get_priority_max(SCHED_OTHER);
+ const s32 min_prio = sched_get_priority_min(SCHED_OTHER);
if (max_prio > min_prio) {
- params.sched_priority = min_prio + ((max_prio - min_prio) * level) / 4;
- } else {
- params.sched_priority = min_prio - ((min_prio - max_prio) * level) / 4;
+ const u32 level = (std::min)(static_cast<u32>(new_priority), 4U);
+ sched_param params{};
+ params.sched_priority =
+ min_prio + static_cast<s32>(static_cast<u32>(max_prio - min_prio) * level) / 4;
+ pthread_setschedparam(pthread_self(), SCHED_OTHER, &params);
}
-
- pthread_setschedparam(this_thread, scheduling_type, &params);
#endif
}
@@ -132,29 +539,94 @@ void SetCurrentThreadName(const char* name) {
#endif
}
-void PinCurrentThreadToPerformanceCore(size_t core_id) {
- ASSERT(core_id < 4);
- // If we set a flag for a CPU that doesn't exist, the thread may not be allowed to
- // run in ANY processor!
- auto const total_cores = std::thread::hardware_concurrency();
- if (core_id < total_cores) {
+void SetCurrentThreadToPerformanceCores() {
#if defined(__ANDROID__)
- cpu_set_t set;
- CPU_ZERO(&set);
- CPU_SET(core_id, &set);
- sched_setaffinity(pthread_self(), sizeof(set), &set);
-#elif defined(__linux__) || defined(__FreeBSD__)
- cpu_set_t set;
- CPU_ZERO(&set);
- CPU_SET(core_id, &set);
- pthread_setaffinity_np(pthread_self(), sizeof(set), &set);
-#elif defined(_WIN32)
- DWORD set = 1UL << core_id;
- SetThreadAffinityMask(GetCurrentThread(), set);
-#else
- // No pin functionality implemented
+ if (ADPF::AddCurrentThread(ADPF::Session::Render)) {
+ SetCurrentThreadCoreGroup(CoreGroup::Unrestricted);
+ return;
+ }
+ SetCurrentThreadCoreGroup(CoreGroup::Performance);
+#endif
+}
+
+void SetCurrentThreadToEfficiencyCores() {
+#if defined(__ANDROID__)
+ if (ADPF::AddCurrentThread(ADPF::Session::Background)) {
+ SetCurrentThreadCoreGroup(CoreGroup::Unrestricted);
+ return;
+ }
+ SetCurrentThreadCoreGroup(CoreGroup::Efficiency);
+#endif
+}
+
+void SetCurrentThreadToBackgroundWork() {
+#if defined(__ANDROID__)
+ ADPF::AddCurrentThread(ADPF::Session::Background);
+ SetCurrentThreadCoreGroup(CoreGroup::Unrestricted);
+#endif
+}
+
+void SetCurrentThreadToAllCores() {
+#if defined(__ANDROID__)
+ ADPF::RemoveCurrentThread();
+ SetCurrentThreadCoreGroup(CoreGroup::Unrestricted);
+#endif
+}
+
+void RefreshThreadPolicies() {
+#if defined(__ANDROID__)
+ CpuTopologyState& state = State();
+ std::scoped_lock topology_lock{state.topology_mutex};
+ RefreshTopologyLocked(state);
+
+ std::scoped_lock policy_lock{state.policy_mutex};
+ std::erase_if(state.policies, [&state](const ThreadPolicy& policy) {
+ bool gone = false;
+ if (policy.has_nice &&
+ setpriority(PRIO_PROCESS, static_cast<id_t>(policy.tid), policy.nice_value) != 0 &&
+ errno == ESRCH) {
+ gone = true;
+ }
+ if (!gone) {
+ ApplyCoreGroupLocked(state, policy.tid, policy.group, &gone);
+ }
+ return gone;
+ });
+ SnapshotCanaryLocked(state);
#endif
+}
+
+void PollThreadPolicies() {
+#if defined(__ANDROID__)
+ static const bool needed = !KernelPreservesRequestedAffinity();
+ if (!needed) {
+ return;
+ }
+
+ CpuTopologyState& state = State();
+ if (!DueForPoll(state)) {
+ return;
+ }
+
+ pid_t tid;
+ cpu_set_t expected;
+ {
+ std::scoped_lock lock{state.topology_mutex};
+ if (!state.canary_valid) {
+ return;
+ }
+ tid = state.canary_tid;
+ expected = state.canary_mask;
}
+
+ cpu_set_t current;
+ CPU_ZERO(&current);
+ if (sched_getaffinity(tid, sizeof(current), &current) == 0 &&
+ std::memcmp(&current, &expected, sizeof(current)) == 0) {
+ return;
+ }
+ RefreshThreadPolicies();
+#endif
}
#ifdef ARCHITECTURE_x86_64
diff --git a/src/common/thread.h b/src/common/thread.h
index a75e342802..4a36295931 100644
--- a/src/common/thread.h
+++ b/src/common/thread.h
@@ -99,8 +99,20 @@ enum class ThreadPriority : u32 {
Critical = 4,
};
+enum class ThreadPlacement : u32 {
+ Default = 0,
+ Background = 1,
+ Efficiency = 2,
+};
+
void SetCurrentThreadPriority(ThreadPriority new_priority);
void SetCurrentThreadName(const char* name);
-void PinCurrentThreadToPerformanceCore(size_t core_id);
+void SetCurrentThreadToPerformanceCores();
+void SetCurrentThreadToEfficiencyCores();
+void SetCurrentThreadToBackgroundWork();
+void SetCurrentThreadToAllCores();
+
+void RefreshThreadPolicies();
+void PollThreadPolicies();
} // namespace Common
diff --git a/src/common/thread_worker.h b/src/common/thread_worker.h
index 6ec9d6a2bd..ec3ca0af49 100644
--- a/src/common/thread_worker.h
+++ b/src/common/thread_worker.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 2020 yuzu Emulator Project
@@ -37,10 +37,25 @@ class StatefulThreadWorker {
using StateMaker = std::conditional_t<with_state, std::function<StateType()>, DummyCallable>;
public:
- explicit StatefulThreadWorker(size_t num_workers, std::string name, StateMaker func = {})
+ explicit StatefulThreadWorker(size_t num_workers, std::string name, StateMaker func = {},
+ ThreadPlacement placement = ThreadPlacement::Default)
: workers_queued{num_workers}, thread_name{std::move(name)} {
- const auto lambda = [this, func](std::stop_token stop_token) {
+ const auto lambda = [this, func, placement](std::stop_token stop_token) {
Common::SetCurrentThreadName(thread_name.c_str());
+ if (placement != ThreadPlacement::Default) {
+ Common::SetCurrentThreadPriority(ThreadPriority::Low);
+ }
+ switch (placement) {
+ case ThreadPlacement::Efficiency:
+ Common::SetCurrentThreadToEfficiencyCores();
+ break;
+ case ThreadPlacement::Background:
+ Common::SetCurrentThreadToBackgroundWork();
+ break;
+ default:
+ Common::SetCurrentThreadToAllCores();
+ break;
+ }
{
[[maybe_unused]] std::conditional_t<with_state, StateType, int> state{func()};
while (!stop_token.stop_requested()) {
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 3ff42cf6f5..d110aa5f8b 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -10,6 +10,7 @@
#include "audio_core/audio_core.h"
#include "common/fs/fs.h"
#include "common/logging.h"
+#include "common/adpf.h"
#include "common/settings.h"
#include "common/settings_enums.h"
#include "common/string_util.h"
@@ -385,6 +386,7 @@ struct System::Impl {
void ShutdownMainProcess() {
SetShuttingDown(true);
+ Common::ADPF::Shutdown();
// Reset per-game flags
Settings::values.use_squashed_iterated_blend = false;
diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp
index 3e4e7bdc68..d2ee6769cc 100644
--- a/src/core/core_timing.cpp
+++ b/src/core/core_timing.cpp
@@ -23,6 +23,21 @@ namespace Core::Timing {
constexpr s64 MAX_SLICE_LENGTH = 10000;
+constexpr u32 CPU_CLOCK_BASE_MHZ = 1020;
+constexpr u32 CPU_CLOCK_BOOST_MHZ = 1734;
+constexpr u32 CPU_CLOCK_OVERCLOCK_MHZ = 2040;
+
+constexpr u32 CpuClockTargetMhz(Settings::CpuClock clock) {
+ switch (clock) {
+ case Settings::CpuClock::Boost:
+ return CPU_CLOCK_BOOST_MHZ;
+ case Settings::CpuClock::Overclock:
+ return CPU_CLOCK_OVERCLOCK_MHZ;
+ default:
+ return CPU_CLOCK_BASE_MHZ;
+ }
+}
+
std::shared_ptr<EventType> CreateEvent(std::string name, TimedCallback&& callback) {
return std::make_shared<EventType>(std::move(callback), std::move(name));
}
@@ -58,7 +73,8 @@ void CoreTiming::Initialize(std::function<void()>&& on_thread_init_) {
if (is_multicore) {
timer_thread = std::jthread([this](std::stop_token stop_token) {
Common::SetCurrentThreadName("HostTiming");
- Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
+ Common::SetCurrentThreadPriority(Common::ThreadPriority::VeryHigh);
+ Common::SetCurrentThreadToPerformanceCores();
on_thread_init();
has_started = true;
@@ -209,8 +225,9 @@ void CoreTiming::ResetTicks() {
u64 CoreTiming::GetClockTicks() const {
u64 fres = is_multicore ? Common::g_wall_clock.GetCNTPCT() : Common::WallClock::CPUTickToCNTPCT(cpu_ticks);
- if (auto const overclock = Settings::values.fast_cpu_time.GetValue(); overclock != Settings::CpuClock::Off) {
- fres = u64(f64(fres) * (1.7 + 0.3 * u32(overclock)));
+ if (const u32 target = CpuClockTargetMhz(Settings::values.cpu_clock.GetValue());
+ target != CPU_CLOCK_BASE_MHZ) {
+ fres = fres * target / CPU_CLOCK_BASE_MHZ;
}
if (::Settings::values.sync_core_speed.GetValue()) {
auto const ticks = f64(fres);
diff --git a/src/core/cpu_manager.cpp b/src/core/cpu_manager.cpp
index 7194fa5022..23d5ac3dfd 100644
--- a/src/core/cpu_manager.cpp
+++ b/src/core/cpu_manager.cpp
@@ -174,12 +174,7 @@ void CpuManager::RunThread(std::stop_token token, std::size_t core) {
std::string name = is_multicore ? ("CPUCore_" + std::to_string(core)) : std::string{"CPUThread"};
Common::SetCurrentThreadName(name.c_str());
Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical);
-#ifdef __ANDROID__
- // Aimed specifically for Snapdragon 8 Elite devices
- // This kills performance on desktop, but boosts perf for UMA devices
- // like the S8E. Mediatek and Mali likely won't suffer.
- Common::PinCurrentThreadToPerformanceCore(core);
-#endif
+ Common::SetCurrentThreadToPerformanceCores();
auto& data = core_data[core];
data.host_context = Common::Fiber::ThreadToFiber();
diff --git a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
index bdbb16ed04..6e9fe9ec07 100644
--- a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
@@ -6,6 +6,7 @@
#include <boost/container/small_vector.hpp>
+#include "common/adpf.h"
#include "common/assert.h"
#include "common/logging.h"
#include "core/core.h"
@@ -86,11 +87,16 @@ void nvdisp_disp0::Composite(std::span<const Nvnflinger::HwcLayer> sorted_layers
}
system.GPU().RequestComposite(std::move(output_layers), std::move(output_fences));
+ Common::ADPF::ReportFrameInterval();
system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs());
system.GetPerfStats().EndSystemFrame();
system.GetPerfStats().BeginSystemFrame();
}
+void nvdisp_disp0::WaitForComposite() {
+ system.GPU().WaitForComposite();
+}
+
Kernel::KEvent* nvdisp_disp0::QueryEvent(u32 event_id) {
LOG_CRITICAL(Service_NVDRV, "Unknown DISP Event {}", event_id);
return nullptr;
diff --git a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
index 1082b85c2b..ff0bfb2ec9 100644
--- a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
+++ b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -37,6 +40,8 @@ public:
/// Performs a screen flip, compositing each buffer.
void Composite(std::span<const Nvnflinger::HwcLayer> sorted_layers);
+ void WaitForComposite();
+
Kernel::KEvent* QueryEvent(u32 event_id) override;
private:
diff --git a/src/core/hle/service/nvnflinger/hardware_composer.cpp b/src/core/hle/service/nvnflinger/hardware_composer.cpp
index cfd999497b..e491b24a8f 100644
--- a/src/core/hle/service/nvnflinger/hardware_composer.cpp
+++ b/src/core/hle/service/nvnflinger/hardware_composer.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 2024 yuzu Emulator Project
@@ -57,18 +57,8 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
// Set default speed limit to 100%.
*out_speed_scale = 1.0f;
- // If no layers are available, skip the logic.
- bool any_visible = false;
- for (auto& layer : display.stack.layers) {
- if (layer->visible) {
- any_visible = true;
- break;
- }
- }
- if (!any_visible) {
- *out_speed_scale = 1.0f;
- return 1;
- }
+ nvdisp.WaitForComposite();
+ this->ReleaseFramebuffersLocked(display);
// Determine the number of vsync periods to wait before composing again.
std::optional<s32> swap_interval{};
@@ -158,55 +148,30 @@ u32 HardwareComposer::ComposeLocked(f32* out_speed_scale, Display& display,
nvdisp.Composite(composition_stack);
}
- // Batch framebuffer releases, instead of one-into-one.
- std::vector<std::pair<Layer*, Framebuffer*>> to_release;
- for (auto& [layer_id, framebuffer] : m_framebuffers) {
- if (!framebuffer.is_acquired)
- continue;
-
- auto layer = display.stack.FindLayer(layer_id);
- if (!layer)
- continue;
-
- // Overlay layers always release after every compose
- // Non-overlay layers release based on their swap interval
- if (layer->is_overlay || framebuffer.release_frame_number <= m_frame_number) {
- to_release.emplace_back(layer.get(), &framebuffer);
- }
- }
- for (auto& [layer, framebuffer] : to_release) {
- layer->buffer_item_consumer->ReleaseBuffer(framebuffer->item, android::Fence::NoFence());
- framebuffer->is_acquired = false;
- }
-
// Advance by 1 frame (60 FPS compositing)
m_frame_number += 1;
- // Release any necessary framebuffers (non-overlay layers only, as overlays are already released above).
+ return 1;
+}
+
+void HardwareComposer::ReleaseFramebuffersLocked(Display& display) {
for (auto& [layer_id, framebuffer] : m_framebuffers) {
if (!framebuffer.is_acquired) {
- // Already released.
continue;
}
- if (framebuffer.release_frame_number > m_frame_number) {
+ const auto layer = display.stack.FindLayer(layer_id);
+ if (!layer) {
continue;
}
- if (const auto layer = display.stack.FindLayer(layer_id); layer != nullptr) {
- // Skip overlay layers as they were already released above
- if (layer->is_overlay) {
- continue;
- }
-
- // TODO: support release fence
- // This is needed to prevent screen tearing
- layer->buffer_item_consumer->ReleaseBuffer(framebuffer.item, android::Fence::NoFence());
- framebuffer.is_acquired = false;
+ if (!layer->is_overlay && framebuffer.release_frame_number > m_frame_number) {
+ continue;
}
- }
- return 1;
+ layer->buffer_item_consumer->ReleaseBuffer(framebuffer.item, android::Fence::NoFence());
+ framebuffer.is_acquired = false;
+ }
}
void HardwareComposer::RemoveLayerLocked(Display& display, ConsumerId consumer_id) {
diff --git a/src/core/hle/service/nvnflinger/hardware_composer.h b/src/core/hle/service/nvnflinger/hardware_composer.h
index e9b7194612..9702e0584b 100644
--- a/src/core/hle/service/nvnflinger/hardware_composer.h
+++ b/src/core/hle/service/nvnflinger/hardware_composer.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 2024 yuzu Emulator Project
@@ -52,6 +52,7 @@ private:
private:
bool TryAcquireFramebufferLocked(Layer& layer, Framebuffer& framebuffer);
CacheStatus CacheFramebufferLocked(Layer& layer, ConsumerId consumer_id);
+ void ReleaseFramebuffersLocked(Display& display);
};
} // namespace Service::Nvnflinger
diff --git a/src/core/hle/service/vi/conductor.cpp b/src/core/hle/service/vi/conductor.cpp
index 002c653b8c..fd7233cf93 100644
--- a/src/core/hle/service/vi/conductor.cpp
+++ b/src/core/hle/service/vi/conductor.cpp
@@ -4,7 +4,11 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <algorithm>
+
+#include "common/adpf.h"
#include "common/settings.h"
+#include "common/thread.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/service/vi/conductor.h"
@@ -14,6 +18,8 @@
constexpr auto FrameNs = std::chrono::nanoseconds{1000000000 / 60};
+constexpr s64 UNLOCKED_TARGET_DIVISOR = 4;
+
namespace Service::VI {
Conductor::Conductor(Core::System& system, Container& container, DisplayList& displays)
@@ -68,6 +74,9 @@ void Conductor::UnlinkVsyncEvent(u64 display_id, Event* event) {
}
void Conductor::ProcessVsync() {
+ Common::PollThreadPolicies();
+ Common::ADPF::SetTargetWorkDuration(std::chrono::nanoseconds{this->GetFramePeriodNs()});
+
for (auto& [display_id, manager] : m_vsync_managers) {
m_container.ComposeOnDisplay(&m_swap_interval, &m_compose_speed_scale, display_id);
manager.SignalVsync(m_system.Kernel());
@@ -76,6 +85,8 @@ void Conductor::ProcessVsync() {
void Conductor::VsyncThread(std::stop_token token) {
Common::SetCurrentThreadName("VSyncThread");
+ Common::SetCurrentThreadPriority(Common::ThreadPriority::VeryHigh);
+ Common::SetCurrentThreadToPerformanceCores();
while (!token.stop_requested()) {
m_signal.Wait();
@@ -114,4 +125,25 @@ s64 Conductor::GetNextTicks() const {
return static_cast<s64>(speed_scale * (1000000000.f / effective_fps));
}
+s64 Conductor::GetFramePeriodNs() const {
+ const auto& settings = Settings::values;
+ f32 speed_scale = 1.f;
+ bool unlocked = false;
+ if (settings.use_multi_core.GetValue()) {
+ if (settings.use_speed_limit.GetValue()) {
+ speed_scale = 100.f / Settings::SpeedLimit();
+ } else {
+ unlocked = true;
+ }
+ }
+ speed_scale /= m_compose_speed_scale;
+
+ const f32 effective_fps = 60.f / static_cast<f32>(m_swap_interval);
+ s64 period = static_cast<s64>(speed_scale * (1000000000.f / effective_fps));
+ if (unlocked) {
+ period /= UNLOCKED_TARGET_DIVISOR;
+ }
+ return std::clamp<s64>(period, 1'000'000, 100'000'000);
+}
+
} // namespace Service::VI
diff --git a/src/core/hle/service/vi/conductor.h b/src/core/hle/service/vi/conductor.h
index 59eb2fd022..7c93c230f7 100644
--- a/src/core/hle/service/vi/conductor.h
+++ b/src/core/hle/service/vi/conductor.h
@@ -44,6 +44,7 @@ private:
void ProcessVsync();
void VsyncThread(std::stop_token token);
s64 GetNextTicks() const;
+ s64 GetFramePeriodNs() const;
private:
Core::System& m_system;
diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp
index 893e4a0854..26597d412c 100644
--- a/src/qt_common/config/shared_translation.cpp
+++ b/src/qt_common/config/shared_translation.cpp
@@ -92,12 +92,9 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
tr("Change the accuracy of the emulated CPU (for debugging only)."));
INSERT(Settings, cpu_backend, tr("Backend:"), QString());
- INSERT(Settings, fast_cpu_time, tr("CPU Overclock"),
- tr("Overclocks the emulated CPU to remove some FPS limiters. Weaker CPUs may see "
- "reduced performance, "
- "and certain games may behave improperly.\nUse Boost (1700MHz) to run at the "
- "Switch's highest native "
- "clock, or Fast (2000MHz) to run at 2x clock."));
+ INSERT(Settings, cpu_clock, tr("CPU Clocks"),
+ tr("Raises the clock the emulated CPU reports, which removes some FPS limiters.\n"
+ "Weaker CPUs may see reduced performance, and certain games may behave improperly."));
INSERT(Settings, use_custom_cpu_ticks, QString(), QString());
INSERT(Settings, cpu_ticks, tr("Custom CPU Ticks"),
@@ -230,9 +227,9 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
tr("Preserves GPU-modified data by reading it back before uploading.\nSome games require this to render certain effects properly."));
INSERT(Settings, use_asynchronous_shaders, tr("Enable asynchronous shader compilation"),
tr("May reduce shader stutter."));
- INSERT(Settings, fast_gpu_time, tr("Fast GPU Time"),
- tr("Overclocks the emulated GPU to increase dynamic resolution and render "
- "distance.\nUse 256 for maximal performance and 512 for maximal graphics fidelity."));
+ INSERT(Settings, gpu_clock, tr("GPU Clocks"),
+ tr("Makes the game believe GPU work finishes faster than it does, so it stops lowering "
+ "resolution and render distance to fit the Switch's clocks."));
INSERT(Settings, gpu_unswizzle_enabled, tr("GPU Unswizzle"),
tr("Accelerates BCn 3D texture decoding using GPU compute.\n"
"Disable if experiencing crashes or graphical glitches."));
@@ -639,9 +636,9 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
}});
translations->insert({Settings::EnumMetadata<Settings::CpuClock>::Index(),
{
- PAIR(CpuClock, Off, tr("Off")),
- PAIR(CpuClock, Boost, tr("Boost (1700MHz)")),
- PAIR(CpuClock, Fast, tr("Fast (2000MHz)")),
+ PAIR(CpuClock, Normal, tr("Normal")),
+ PAIR(CpuClock, Boost, tr("Boost")),
+ PAIR(CpuClock, Overclock, tr("Overclock")),
}});
translations->insert(
{Settings::EnumMetadata<Settings::ConfirmStop>::Index(),
@@ -650,11 +647,11 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
PAIR(ConfirmStop, Ask_Based_On_Game, tr("Only if game specifies not to stop")),
PAIR(ConfirmStop, Ask_Never, tr("Never ask")),
}});
- translations->insert({Settings::EnumMetadata<Settings::GpuOverclock>::Index(),
+ translations->insert({Settings::EnumMetadata<Settings::GpuClock>::Index(),
{
- PAIR(GpuOverclock, Normal, tr("Off")),
- PAIR(GpuOverclock, Medium, tr("Medium (256)")),
- PAIR(GpuOverclock, High, tr("High (512)")),
+ PAIR(GpuClock, Normal, tr("Normal")),
+ PAIR(GpuClock, Boost, tr("Boost")),
+ PAIR(GpuClock, Overclock, tr("Overclock")),
}});
translations->insert({Settings::EnumMetadata<Settings::GpuUnswizzleSize>::Index(),
{
diff --git a/src/video_core/fence_manager.h b/src/video_core/fence_manager.h
index 16f19bf009..885c714025 100644
--- a/src/video_core/fence_manager.h
+++ b/src/video_core/fence_manager.h
@@ -195,6 +195,7 @@ private:
void ReleaseThreadFunc(std::stop_token stop_token) {
Common::SetCurrentThreadName("GPUFencingThread");
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
+ Common::SetCurrentThreadToPerformanceCores();
TFence current_fence;
std::deque<std::function<void()>> current_operations;
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index 548a8bf809..a44015c0dd 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -10,6 +10,7 @@
#include <condition_variable>
#include <list>
#include <memory>
+#include <utility>
#include "common/assert.h"
#include "common/settings.h"
@@ -39,6 +40,19 @@
namespace Tegra {
+namespace {
+constexpr u64 GpuClockMultiplier(Settings::GpuClock clock) {
+ switch (clock) {
+ case Settings::GpuClock::Boost:
+ return 256;
+ case Settings::GpuClock::Overclock:
+ return 512;
+ default:
+ return 1;
+ }
+}
+} // Anonymous namespace
+
struct GPU::Impl {
explicit Impl(Core::System& system_, bool is_async_, bool use_nvdec_)
: system{system_}
@@ -116,7 +130,7 @@ struct GPU::Impl {
[[nodiscard]] u64 RequestSyncOperation(Func&& action) {
std::unique_lock lck{sync_request_mutex};
const u64 fence = ++last_sync_fence;
- sync_requests.emplace_back(action);
+ sync_requests.emplace_back(std::forward<Func>(action));
return fence;
}
@@ -145,14 +159,8 @@ struct GPU::Impl {
}
[[nodiscard]] u64 GetTicks() const {
- u64 gpu_tick = system.CoreTiming().GetGPUTicks();
- Settings::GpuOverclock overclock = Settings::values.fast_gpu_time.GetValue();
-
- if (overclock != Settings::GpuOverclock::Normal) {
- gpu_tick /= 256 * u64(overclock);
- }
-
- return gpu_tick;
+ const u64 gpu_tick = system.CoreTiming().GetGPUTicks();
+ return gpu_tick / GpuClockMultiplier(Settings::values.gpu_clock.GetValue());
}
void RendererFrameEndNotify() {
@@ -225,9 +233,9 @@ struct GPU::Impl {
}
void RequestComposite(std::vector<Tegra::FramebufferConfig>&& layers, std::vector<Service::Nvidia::NvFence>&& fences) {
- size_t num_fences{fences.size()};
+ const size_t num_fences{fences.size()};
size_t current_request_counter{};
- {
+ if (num_fences != 0) {
std::unique_lock<std::mutex> lk(request_swap_mutex);
if (free_swap_counters.empty()) {
current_request_counter = request_swap_counters.size();
@@ -238,27 +246,42 @@ struct GPU::Impl {
free_swap_counters.pop_front();
}
}
- const auto wait_fence = RequestSyncOperation([this, current_request_counter, &layers, &fences, num_fences] {
- auto& syncpoint_manager = system.Host1x().GetSyncpointManager();
- if (num_fences == 0) {
- renderer->Composite(layers);
- }
- const auto executer = [this, current_request_counter, layers_copy = layers]() {
- {
- std::unique_lock<std::mutex> lk(request_swap_mutex);
- if (--request_swap_counters[current_request_counter] != 0) {
- return;
+ pending_composite_fence = RequestSyncOperation(
+ [this, current_request_counter, num_fences, composite_layers = std::move(layers),
+ composite_fences = std::move(fences)] {
+ if (num_fences == 0) {
+ renderer->Composite(composite_layers);
+ return;
+ }
+ auto& syncpoint_manager = system.Host1x().GetSyncpointManager();
+ const auto executer = [this, current_request_counter, composite_layers]() {
+ {
+ std::unique_lock<std::mutex> lk(request_swap_mutex);
+ if (--request_swap_counters[current_request_counter] != 0) {
+ return;
+ }
+ free_swap_counters.push_back(current_request_counter);
}
- free_swap_counters.push_back(current_request_counter);
+ renderer->Composite(composite_layers);
+ };
+ for (size_t i = 0; i < num_fences; i++) {
+ syncpoint_manager.RegisterGuestAction(composite_fences[i].id,
+ composite_fences[i].value, executer);
}
- renderer->Composite(layers_copy);
- };
- for (size_t i = 0; i < num_fences; i++) {
- syncpoint_manager.RegisterGuestAction(fences[i].id, fences[i].value, executer);
- }
- });
+ });
gpu_thread.TickGPU(is_async);
- WaitForSyncOperation(wait_fence);
+ }
+
+ void WaitForComposite() {
+ const u64 fence = pending_composite_fence;
+ if (fence == 0) {
+ return;
+ }
+ pending_composite_fence = 0;
+ if (shutting_down.load(std::memory_order_relaxed)) {
+ return;
+ }
+ WaitForSyncOperation(fence);
}
std::vector<u8> GetAppletCaptureBuffer() {
@@ -311,6 +334,7 @@ struct GPU::Impl {
std::deque<size_t> free_swap_counters;
std::deque<size_t> request_swap_counters;
std::mutex request_swap_mutex;
+ u64 pending_composite_fence{};
};
GPU::GPU(Core::System& system, bool is_async, bool use_nvdec)
@@ -428,6 +452,10 @@ void GPU::RequestComposite(std::vector<Tegra::FramebufferConfig>&& layers,
impl->RequestComposite(std::move(layers), std::move(fences));
}
+void GPU::WaitForComposite() {
+ impl->WaitForComposite();
+}
+
std::vector<u8> GPU::GetAppletCaptureBuffer() {
return impl->GetAppletCaptureBuffer();
}
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index 538c4da85a..8d4a101fd4 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.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 2018 yuzu Emulator Project
@@ -218,6 +218,8 @@ public:
void RequestComposite(std::vector<Tegra::FramebufferConfig>&& layers,
std::vector<Service::Nvidia::NvFence>&& fences);
+ void WaitForComposite();
+
std::vector<u8> GetAppletCaptureBuffer();
/// Performs any additional setup necessary in order to begin GPU emulation.
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index 50570b596d..2acf9ac458 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -30,6 +30,7 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten
thread = std::jthread([&](std::stop_token stop_token) {
Common::SetCurrentThreadName("GPU");
Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical);
+ Common::SetCurrentThreadToPerformanceCores();
system.RegisterHostThread();
auto current_context = context.Acquire();
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 2772a68d7a..785602b33a 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -305,7 +305,7 @@ size_t GetTotalPipelineWorkers() {
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
#ifdef __ANDROID__
const int configured = AndroidSettings::values.pipeline_worker_count.GetValue();
- const int clamped = std::clamp(configured, 4, 8);
+ const int clamped = std::clamp(configured, 2, 8);
const size_t desired = static_cast<size_t>(clamped);
if (desired == 0) {
return 1ULL;
@@ -351,8 +351,9 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
use_asynchronous_shaders{Settings::values.use_asynchronous_shaders.GetValue()},
use_vulkan_pipeline_cache{Settings::values.use_vulkan_driver_pipeline_cache.GetValue()},
workers(device.HasBrokenParallelShaderCompiling() ? 1ULL : GetTotalPipelineWorkers(),
- "VkPipelineBuilder"),
- serialization_thread(1, "VkPipelineSerialization") {
+ "VkPipelineBuilder", {}, Common::ThreadPlacement::Background),
+ serialization_thread(1, "VkPipelineSerialization", {},
+ Common::ThreadPlacement::Background) {
const auto& float_control{device.FloatControlProperties()};
const VkDriverId driver_id{device.GetDriverID()};
const VkShaderStageFlags subgroup_stages{device.GetSubgroupSupportedStages()};
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp
index 53c3f6759f..90ebb55d38 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.cpp
+++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp
@@ -266,6 +266,8 @@ void PresentManager::WaitPresent() {
void PresentManager::PresentThread(std::stop_token token) {
Common::SetCurrentThreadName("VulkanPresent");
+ Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
+ Common::SetCurrentThreadToPerformanceCores();
while (!token.stop_requested()) {
std::unique_lock lock{queue_mutex};
// Wait for presentation frames
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp
index 7432c94cfc..3601a3e891 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.cpp
+++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp
@@ -258,6 +258,8 @@ bool Scheduler::UpdateDescriptorBufferChunk(u32 descriptor_chunk) {
void Scheduler::WorkerThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("VulkanWorker");
+ Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical);
+ Common::SetCurrentThreadToPerformanceCores();
const auto TryPopQueue{[this](auto& work) -> bool {
if (work_queue.empty()) {
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index fbc2bb4cf7..fafc331ef7 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -509,7 +509,8 @@ private:
u64 frame_tick = 0;
u64 last_sampler_gc_frame = (std::numeric_limits<u64>::max)();
- Common::ThreadWorker texture_decode_worker{1, "TextureDecoder"};
+ Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {},
+ Common::ThreadPlacement::Efficiency};
std::vector<std::unique_ptr<AsyncDecodeContext>> async_decodes;
std::deque<PendingUnswizzle> unswizzle_queue;
diff --git a/src/video_core/textures/workers.cpp b/src/video_core/textures/workers.cpp
index 780e0c4da9..a1953c4bf4 100644
--- a/src/video_core/textures/workers.cpp
+++ b/src/video_core/textures/workers.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 2023 yuzu Emulator Project
@@ -11,7 +11,6 @@ namespace Tegra::Texture {
Common::ThreadWorker& GetThreadWorkers() {
static Common::ThreadWorker workers{(std::max)(std::thread::hardware_concurrency(), 2U) / 2,
"ImageTranscode"};
-
return workers;
}
diff --git a/src/yuzu/configuration/configure_cpu.cpp b/src/yuzu/configuration/configure_cpu.cpp
index 0511465ee6..8c7b15e4e0 100644
--- a/src/yuzu/configuration/configure_cpu.cpp
+++ b/src/yuzu/configuration/configure_cpu.cpp
@@ -76,8 +76,7 @@ void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
} else if (setting->Id() == Settings::values.cpu_backend.Id()) {
backend_layout->addWidget(widget);
backend_combobox = widget->combobox;
- } else if (setting->Id() == Settings::values.fast_cpu_time.Id() ||
- setting->Id() == Settings::values.cpu_ticks.Id()) {
+ } else if (setting->Id() == Settings::values.cpu_ticks.Id()) {
ui->general_layout->addWidget(widget);
} else {
// Presently, all other settings here are unsafe checkboxes