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/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(Settings::getDebugKnobAt(static_cast(index))); } 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..bfcd0d311f --- /dev/null +++ b/src/common/adpf.cpp @@ -0,0 +1,307 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/adpf.h" + +#ifdef __ANDROID__ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#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(load("APerformanceHint_getManager")); + resolved.create_session = + reinterpret_cast(load("APerformanceHint_createSession")); + resolved.close_session = + reinterpret_cast(load("APerformanceHint_closeSession")); + resolved.update_target = + reinterpret_cast(load("APerformanceHint_updateTargetWorkDuration")); + resolved.report_actual = + reinterpret_cast(load("APerformanceHint_reportActualWorkDuration")); + resolved.set_threads = + reinterpret_cast(load("APerformanceHint_setThreads")); + resolved.set_power_efficiency = reinterpret_cast( + 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 threads; + bool unsupported = false; +}; + +std::mutex g_mutex; +std::array g_sessions; + +std::atomic g_target_ns{DEFAULT_TARGET.count()}; +thread_local std::chrono::steady_clock::time_point t_work_begin{}; + +SessionState& StateOf(Session session) { + return g_sessions[static_cast(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; + } +} + +bool OpenLocked(Session session, SessionState& state) { + const Api& api = Resolve(); + const s64 target = session == Session::Render ? g_target_ns.load(std::memory_order_relaxed) : 0; + + std::vector ids; + ids.reserve(state.threads.size()); + for (const pid_t tid : state.threads) { + ids.push_back(static_cast(tid)); + } + + state.handle = api.create_session(api.manager, ids.data(), ids.size(), target); + if (state.handle == nullptr && target == 0) { + state.handle = + api.create_session(api.manager, ids.data(), ids.size(), DEFAULT_TARGET.count()); + } + if (state.handle == nullptr) { + state.unsupported = true; + LOG_WARNING(Common, "Could not open a performance hint session, falling back"); + return false; + } + if (session == Session::Background) { + api.set_power_efficiency(state.handle, true); + } + return true; +} + +bool SyncLocked(Session session, SessionState& state) { + if (state.threads.empty()) { + CloseLocked(state); + return false; + } + if (state.handle == nullptr) { + return OpenLocked(session, state); + } + + const Api& api = Resolve(); + if (api.set_threads != nullptr) { + std::vector ids = state.threads; + if (api.set_threads(state.handle, ids.data(), ids.size()) == 0) { + return true; + } + } + CloseLocked(state); + return OpenLocked(session, state); +} + +} // 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(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(i), state); + } + } + + SessionState& state = StateOf(session); + if (std::find(state.threads.begin(), state.threads.end(), tid) == state.threads.end()) { + state.threads.push_back(tid); + } + if (!SyncLocked(session, state)) { + 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(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 BeginFrameWork() { + if (!Resolve().usable) { + return; + } + t_work_begin = std::chrono::steady_clock::now(); +} + +void EndFrameWork() { + const Api& api = Resolve(); + if (!api.usable || t_work_begin.time_since_epoch().count() == 0) { + return; + } + const auto elapsed = std::chrono::steady_clock::now() - t_work_begin; + t_work_begin = {}; + + const s64 actual = std::chrono::duration_cast(elapsed).count(); + if (actual <= 0) { + return; + } + + 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 BeginFrameWork() {} + +void EndFrameWork() {} + +void Shutdown() {} + +} // namespace Common::ADPF + +#endif diff --git a/src/common/adpf.h b/src/common/adpf.h new file mode 100644 index 0000000000..8a74525e51 --- /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 + +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 BeginFrameWork(); +void EndFrameWork(); + +void Shutdown(); + +} // namespace Common::ADPF diff --git a/src/common/thread.cpp b/src/common/thread.cpp index c947c88373..89beaa96e3 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 #include +#include "common/adpf.h" #include "common/error.h" #include "common/logging.h" #include "common/assert.h" @@ -39,6 +41,414 @@ #include #endif +#ifdef __linux__ +#include +#include + +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 == RLIM_INFINITY) { + return -20; + } + return 20 - static_cast(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, LowestAllowedNice()); +} +} // Anonymous namespace +#endif + +#ifdef __ANDROID__ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr size_t ANDROID_MINIMUM_PERFORMANCE_CORES = 4; + +constexpr std::chrono::nanoseconds ANDROID_POLICY_POLL_INTERVAL = std::chrono::milliseconds{500}; + +enum class CoreGroup { + Unrestricted, + Performance, + Efficiency, +}; + +struct CoreTopology { + cpu_set_t allowed; + cpu_set_t performance; + cpu_set_t efficiency; + bool separated; + bool initialized; +}; + +struct ThreadPolicy { + pid_t tid; + CoreGroup group; + int nice_value; + bool has_nice; +}; + +struct CoreInfo { + long weight; + u64 midr; + int cpu; +}; + +std::mutex g_topology_mutex; +CoreTopology g_topology{}; + +pid_t g_canary_tid = 0; +cpu_set_t g_canary_mask; +bool g_canary_valid = false; + +std::atomic g_next_poll_ns{0}; + +std::mutex g_policy_mutex; + +std::vector& Policies() { + static auto* const policies = new std::vector(); + return *policies; +} + +struct PolicyRegistration { + ~PolicyRegistration() { + const pid_t tid = gettid(); + ::Common::ADPF::RemoveCurrentThread(); + std::scoped_lock lock{g_policy_mutex}; + std::erase_if(Policies(), [tid](const ThreadPolicy& policy) { return policy.tid == tid; }); + } +}; + +thread_local PolicyRegistration t_policy_registration; + +int PossibleCpuCount() { + std::ifstream file("/sys/devices/system/cpu/possible"); + std::string list; + if (file && std::getline(file, list) && !list.empty()) { + int highest = -1; + const char* cursor = list.c_str(); + while (*cursor != '\0') { + char* end = nullptr; + const long value = std::strtol(cursor, &end, 10); + if (end == cursor) { + break; + } + highest = (std::max)(highest, static_cast(value)); + cursor = end; + while (*cursor == '-' || *cursor == ',') { + ++cursor; + } + } + if (highest >= 0) { + return (std::min)(highest + 1, CPU_SETSIZE); + } + } + const long configured = sysconf(_SC_NPROCESSORS_CONF); + if (configured > 0) { + return static_cast((std::min)(configured, CPU_SETSIZE)); + } + return static_cast((std::min)(std::thread::hardware_concurrency(), CPU_SETSIZE)); +} + +long ReadCpuScalar(int cpu, const char* node) { + long 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(int 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 CollectCores(const cpu_set_t& allowed, int total, const char* node, + bool require_all) { + std::vector cores; + for (int cpu = 0; cpu < total; ++cpu) { + if (!CPU_ISSET(cpu, &allowed)) { + continue; + } + const long 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& cores) { + return std::all_of(cores.begin(), cores.end(), + [&](const CoreInfo& core) { return core.weight == cores.front().weight; }); +} + +bool MidrsAreDistinct(const std::vector& cores) { + const auto unknown = [](const CoreInfo& core) { return core.midr == 0; }; + if (std::any_of(cores.begin(), cores.end(), unknown)) { + return false; + } + return std::any_of(cores.begin(), cores.end(), + [&](const CoreInfo& core) { return core.midr != cores.front().midr; }); +} + +void ComputeTopologyLocked() { + g_topology.initialized = true; + g_topology.separated = false; + CPU_ZERO(&g_topology.allowed); + CPU_ZERO(&g_topology.performance); + CPU_ZERO(&g_topology.efficiency); + + if (sched_getaffinity(getpid(), sizeof(g_topology.allowed), &g_topology.allowed) != 0) { + LOG_WARNING(Common, "Could not query process CPU affinity: {}", + ::Common::GetLastErrorMsg()); + return; + } + + const int total = PossibleCpuCount(); + auto cores = CollectCores(g_topology.allowed, total, "cpu_capacity", true); + if (cores.empty() || (WeightsAreUniform(cores) && MidrsAreDistinct(cores))) { + auto by_frequency = + CollectCores(g_topology.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 bool midr_known = std::none_of(cores.begin(), cores.end(), + [](const CoreInfo& core) { return core.midr == 0; }); + + const size_t allowed_count = static_cast(CPU_COUNT(&g_topology.allowed)); + const size_t maximum = + allowed_count > 2 * ANDROID_MINIMUM_PERFORMANCE_CORES + ? allowed_count - ANDROID_MINIMUM_PERFORMANCE_CORES + : ANDROID_MINIMUM_PERFORMANCE_CORES; + + size_t taken = 0; + long cluster_weight = cores.front().weight; + u64 cluster_midr = cores.front().midr; + for (const auto& core : cores) { + if (core.weight != cluster_weight || (midr_known && core.midr != cluster_midr)) { + if (taken >= ANDROID_MINIMUM_PERFORMANCE_CORES) { + break; + } + cluster_weight = core.weight; + cluster_midr = core.midr; + } + if (taken >= maximum) { + break; + } + CPU_SET(core.cpu, &g_topology.performance); + ++taken; + } + if (taken == 0) { + return; + } + + for (int cpu = 0; cpu < total; ++cpu) { + if (CPU_ISSET(cpu, &g_topology.allowed) && !CPU_ISSET(cpu, &g_topology.performance)) { + CPU_SET(cpu, &g_topology.efficiency); + } + } + + g_topology.separated = CPU_COUNT(&g_topology.efficiency) > 0; + LOG_INFO(Common, "CPU topology: {} performance cores, {} efficiency cores, separation {}", + CPU_COUNT(&g_topology.performance), CPU_COUNT(&g_topology.efficiency), + g_topology.separated ? "enabled" : "unavailable"); +} + +void EnsureTopologyLocked() { + if (!g_topology.initialized) { + ComputeTopologyLocked(); + } +} + +void RefreshTopologyLocked() { + if (!g_topology.initialized) { + ComputeTopologyLocked(); + return; + } + cpu_set_t current; + CPU_ZERO(¤t); + if (sched_getaffinity(getpid(), sizeof(current), ¤t) != 0) { + return; + } + if (std::memcmp(¤t, &g_topology.allowed, sizeof(current)) != 0) { + ComputeTopologyLocked(); + } +} + +bool ApplyCoreGroupLocked(pid_t tid, CoreGroup group, bool* gone = nullptr) { + const bool restrict_group = group != CoreGroup::Unrestricted && g_topology.separated; + const cpu_set_t* mask = &g_topology.allowed; + if (restrict_group) { + mask = group == CoreGroup::Performance ? &g_topology.performance : &g_topology.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; + } + int major = 0; + int minor = 0; + if (std::sscanf(info.release, "%d.%d", &major, &minor) != 2) { + return false; + } + return major > 6 || (major == 6 && minor >= 2); +} + +void SnapshotCanaryLocked() { + g_canary_valid = false; + if (!g_topology.separated) { + return; + } + for (const auto& policy : Policies()) { + if (policy.group == CoreGroup::Unrestricted) { + continue; + } + cpu_set_t mask; + CPU_ZERO(&mask); + if (sched_getaffinity(policy.tid, sizeof(mask), &mask) != 0) { + continue; + } + g_canary_tid = policy.tid; + g_canary_mask = mask; + g_canary_valid = true; + return; + } +} + +bool DueForPoll() { + const auto now = std::chrono::steady_clock::now().time_since_epoch(); + const s64 now_ns = std::chrono::duration_cast(now).count(); + s64 next = g_next_poll_ns.load(std::memory_order_relaxed); + if (now_ns < next) { + return false; + } + return g_next_poll_ns.compare_exchange_strong(next, now_ns + ANDROID_POLICY_POLL_INTERVAL.count(), + std::memory_order_relaxed); +} + +ThreadPolicy& AcquirePolicyLocked(pid_t tid) { + auto& policies = Policies(); + for (auto& policy : policies) { + if (policy.tid == tid) { + return policy; + } + } + return 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; + } + + std::scoped_lock topology_lock{g_topology_mutex}; + EnsureTopologyLocked(); + ApplyCoreGroupLocked(tid, effective); + + std::scoped_lock policy_lock{g_policy_mutex}; + AcquirePolicyLocked(tid).group = effective; + if (!g_canary_valid) { + SnapshotCanaryLocked(); + } +} + +void RememberCurrentThreadNice(pid_t tid, int nice_value) { + (void)&t_policy_registration; + std::scoped_lock lock{g_policy_mutex}; + ThreadPolicy& policy = AcquirePolicyLocked(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 +458,6 @@ #endif #include "common/x64/rdtsc.h" #endif -#include "core/core_timing.h" namespace Common { @@ -78,21 +487,31 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) { } }(); set_thread_priority(find_thread(NULL), priority); -#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; - 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; +#elif defined(__ANDROID__) + const int nice_value = NiceValueForPriority(new_priority); + const pid_t tid = gettid(); + if (setpriority(PRIO_PROCESS, static_cast(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 + 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) { + const u32 level = (std::min)(static_cast(new_priority), 4U); + sched_param params{}; + params.sched_priority = + min_prio + static_cast(static_cast(max_prio - min_prio) * level) / 4; + pthread_setschedparam(pthread_self(), SCHED_OTHER, ¶ms); } - - pthread_setschedparam(this_thread, scheduling_type, ¶ms); #endif } @@ -132,29 +551,88 @@ 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 -#endif + 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__) + std::scoped_lock topology_lock{g_topology_mutex}; + RefreshTopologyLocked(); + + std::scoped_lock policy_lock{g_policy_mutex}; + std::erase_if(Policies(), [](const ThreadPolicy& policy) { + bool gone = false; + if (policy.has_nice && + setpriority(PRIO_PROCESS, static_cast(policy.tid), policy.nice_value) != 0 && + errno == ESRCH) { + gone = true; + } + if (!gone) { + ApplyCoreGroupLocked(policy.tid, policy.group, &gone); + } + return gone; + }); + SnapshotCanaryLocked(); +#endif +} + +void PollThreadPolicies() { +#if defined(__ANDROID__) + static const bool needed = !KernelPreservesRequestedAffinity(); + if (!needed || !DueForPoll()) { + return; + } + + pid_t tid; + cpu_set_t expected; + { + std::scoped_lock lock{g_topology_mutex}; + if (!g_canary_valid) { + return; + } + tid = g_canary_tid; + expected = g_canary_mask; + } + + cpu_set_t current; + CPU_ZERO(¤t); + if (sched_getaffinity(tid, sizeof(current), ¤t) == 0 && + std::memcmp(¤t, &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, 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 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..d18c8a72f1 100644 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -58,7 +58,8 @@ void CoreTiming::Initialize(std::function&& 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; 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..9c321fba82 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 +#include "common/adpf.h" #include "common/assert.h" #include "common/logging.h" #include "core/core.h" @@ -86,9 +87,11 @@ void nvdisp_disp0::Composite(std::span sorted_layers } system.GPU().RequestComposite(std::move(output_layers), std::move(output_fences)); + Common::ADPF::EndFrameWork(); system.SpeedLimiter().DoSpeedLimiting(system.CoreTiming().GetGlobalTimeUs()); system.GetPerfStats().EndSystemFrame(); system.GetPerfStats().BeginSystemFrame(); + Common::ADPF::BeginFrameWork(); } Kernel::KEvent* nvdisp_disp0::QueryEvent(u32 event_id) { diff --git a/src/core/hle/service/vi/conductor.cpp b/src/core/hle/service/vi/conductor.cpp index 002c653b8c..1dc13d0113 100644 --- a/src/core/hle/service/vi/conductor.cpp +++ b/src/core/hle/service/vi/conductor.cpp @@ -4,7 +4,9 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#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" @@ -68,6 +70,9 @@ void Conductor::UnlinkVsyncEvent(u64 display_id, Event* event) { } void Conductor::ProcessVsync() { + Common::PollThreadPolicies(); + Common::ADPF::SetTargetWorkDuration(std::chrono::nanoseconds{this->GetNextTicks()}); + 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 +81,8 @@ void Conductor::ProcessVsync() { void Conductor::VsyncThread(std::stop_token token) { Common::SetCurrentThreadName("VSyncThread"); + Common::SetCurrentThreadPriority(Common::ThreadPriority::High); + Common::SetCurrentThreadToPerformanceCores(); while (!token.stop_requested()) { m_signal.Wait(); 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> current_operations; 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(static_cast(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(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..adc1d2fbdb 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::VeryHigh); + 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::max)(); - Common::ThreadWorker texture_decode_worker{1, "TextureDecoder"}; + Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {}, + Common::ThreadPlacement::Efficiency}; std::vector> async_decodes; std::deque unswizzle_queue; diff --git a/src/video_core/textures/workers.cpp b/src/video_core/textures/workers.cpp index 780e0c4da9..9f37dfc48e 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 @@ -10,7 +10,8 @@ namespace Tegra::Texture { Common::ThreadWorker& GetThreadWorkers() { static Common::ThreadWorker workers{(std::max)(std::thread::hardware_concurrency(), 2U) / 2, - "ImageTranscode"}; + "ImageTranscode", {}, + Common::ThreadPlacement::Efficiency}; return workers; }