From 1507074346299d0d49a454839c1f9ad60ef339dc Mon Sep 17 00:00:00 2001 From: CamilleLaVey Date: Sat, 1 Aug 2026 20:49:41 -0400 Subject: [PATCH] Refactor Android CPU affinity/threading --- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 2 + .../yuzu_emu/fragments/EmulationFragment.kt | 1 + src/android/app/src/main/jni/native.cpp | 5 + src/common/thread.cpp | 293 +++++++++++++----- src/common/thread.h | 2 + src/common/thread_worker.h | 4 +- src/core/core_timing.cpp | 3 +- src/core/hle/service/vi/conductor.cpp | 2 + .../renderer_vulkan/vk_present_manager.cpp | 1 + .../renderer_vulkan/vk_scheduler.cpp | 1 + .../texture_cache/texture_cache_base.h | 2 +- src/video_core/textures/workers.cpp | 2 +- 12 files changed, 244 insertions(+), 74 deletions(-) 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/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/thread.cpp b/src/common/thread.cpp index 2b920d3c42..fd5ba319cc 100644 --- a/src/common/thread.cpp +++ b/src/common/thread.cpp @@ -43,103 +43,251 @@ #ifdef __ANDROID__ #include #include +#include +#include #include +#include #include #include namespace { -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_URGENT_AUDIO = -19; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_AUDIO = -16; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_URGENT_DISPLAY = -8; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_DISPLAY = -4; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_FOREGROUND = -2; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_MORE_FAVORABLE = -1; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_DEFAULT = 0; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_LESS_FAVORABLE = 1; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_BACKGROUND = 10; -[[maybe_unused]] constexpr int ANDROID_THREAD_PRIORITY_LOWEST = 19; +constexpr int ANDROID_THREAD_PRIORITY_AUDIO = -16; +constexpr int ANDROID_THREAD_PRIORITY_URGENT_DISPLAY = -8; +constexpr int ANDROID_THREAD_PRIORITY_DISPLAY = -4; +constexpr int ANDROID_THREAD_PRIORITY_DEFAULT = 0; +constexpr int ANDROID_THREAD_PRIORITY_BACKGROUND = 10; constexpr size_t ANDROID_MINIMUM_PERFORMANCE_CORES = 4; -cpu_set_t ComputePerformanceCoreMask() { - cpu_set_t mask; - CPU_ZERO(&mask); +enum class CoreGroup { + Unrestricted, + Performance, + Efficiency, +}; +struct CoreTopology { cpu_set_t allowed; - CPU_ZERO(&allowed); - if (sched_getaffinity(gettid(), sizeof(allowed), &allowed) != 0) { - return mask; + 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; +}; + +std::mutex g_topology_mutex; +CoreTopology g_topology{}; + +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(); + 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; +} +std::vector> CollectCoreWeights(const cpu_set_t& allowed, int total, + const char* node, bool require_all) { std::vector> cores; - const int total = static_cast(std::thread::hardware_concurrency()); for (int cpu = 0; cpu < total; ++cpu) { if (!CPU_ISSET(cpu, &allowed)) { continue; } - long max_frequency = 0; - std::ifstream file("/sys/devices/system/cpu/cpu" + std::to_string(cpu) + - "/cpufreq/cpuinfo_max_freq"); - if (!file || !(file >> max_frequency) || max_frequency <= 0) { - CPU_ZERO(&mask); - return mask; + 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.emplace_back(max_frequency, cpu); + cores.emplace_back(weight, cpu); + } + return cores; +} + +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 = CollectCoreWeights(g_topology.allowed, total, "cpu_capacity", true); + if (cores.empty()) { + cores = CollectCoreWeights(g_topology.allowed, total, "cpufreq/cpuinfo_max_freq", false); } if (cores.empty()) { - return mask; + LOG_WARNING(Common, "Could not determine CPU topology, thread placement is disabled"); + return; } std::sort(cores.begin(), cores.end(), [](const auto& lhs, const auto& rhs) { return lhs.first > rhs.first; }); + 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_frequency = cores.front().first; - for (const auto& [frequency, cpu] : cores) { - if (frequency != cluster_frequency) { + long cluster_weight = cores.front().first; + for (const auto& [weight, cpu] : cores) { + if (weight != cluster_weight) { if (taken >= ANDROID_MINIMUM_PERFORMANCE_CORES) { break; } - cluster_frequency = frequency; + cluster_weight = weight; } - CPU_SET(cpu, &mask); + if (taken >= maximum) { + break; + } + CPU_SET(cpu, &g_topology.performance); ++taken; } - return mask; -} + if (taken == 0) { + return; + } -const cpu_set_t& PerformanceCoreMask() { - static const cpu_set_t mask = ComputePerformanceCoreMask(); - return mask; + 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"); } -cpu_set_t ComputeEfficiencyCoreMask() { - cpu_set_t mask; - CPU_ZERO(&mask); +void EnsureTopologyLocked() { + if (!g_topology.initialized) { + ComputeTopologyLocked(); + } +} - const cpu_set_t& performance = PerformanceCoreMask(); - if (CPU_COUNT(&performance) == 0) { - return mask; +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(); + } +} - cpu_set_t allowed; - CPU_ZERO(&allowed); - if (sched_getaffinity(gettid(), sizeof(allowed), &allowed) != 0) { - return mask; +bool ApplyCoreGroupLocked(pid_t tid, CoreGroup group) { + if (!g_topology.separated || group == CoreGroup::Unrestricted) { + return false; + } + const cpu_set_t& 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) { + LOG_WARNING(Common, "Could not restrict thread {} to its core group: {}", tid, + ::Common::GetLastErrorMsg()); + return false; + } + return true; +} - const int total = static_cast(std::thread::hardware_concurrency()); - for (int cpu = 0; cpu < total; ++cpu) { - if (CPU_ISSET(cpu, &allowed) && !CPU_ISSET(cpu, &performance)) { - CPU_SET(cpu, &mask); +ThreadPolicy& AcquirePolicyLocked(pid_t tid) { + auto& policies = Policies(); + for (auto& policy : policies) { + if (policy.tid == tid) { + return policy; } } - return mask; + return policies.emplace_back(ThreadPolicy{tid, CoreGroup::Unrestricted, 0, false}); } -const cpu_set_t& EfficiencyCoreMask() { - static const cpu_set_t mask = ComputeEfficiencyCoreMask(); - return mask; +void SetCurrentThreadCoreGroup(CoreGroup group) { + const pid_t tid = gettid(); + { + std::scoped_lock lock{g_topology_mutex}; + EnsureTopologyLocked(); + ApplyCoreGroupLocked(tid, group); + } + (void)&t_policy_registration; + std::scoped_lock lock{g_policy_mutex}; + AcquirePolicyLocked(tid).group = group; +} + +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 @@ -153,7 +301,6 @@ const cpu_set_t& EfficiencyCoreMask() { #endif #include "common/x64/rdtsc.h" #endif -#include "core/core_timing.h" namespace Common { @@ -194,10 +341,13 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) { default: return ANDROID_THREAD_PRIORITY_DEFAULT; } }(); - if (setpriority(PRIO_PROCESS, static_cast(gettid()), nice_value) != 0) { - LOG_DEBUG(Common, "Could not set thread nice value to {}: {}", nice_value, - GetLastErrorMsg()); + 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); #else pthread_t this_thread = pthread_self(); const auto scheduling_type = SCHED_OTHER; @@ -254,24 +404,27 @@ void SetCurrentThreadName(const char* name) { void SetCurrentThreadToPerformanceCores() { #if defined(__ANDROID__) - const cpu_set_t& mask = PerformanceCoreMask(); - if (CPU_COUNT(&mask) == 0) { - return; - } - if (sched_setaffinity(gettid(), sizeof(mask), &mask) != 0) { - LOG_DEBUG(Common, "Could not restrict thread to performance cores: {}", GetLastErrorMsg()); - } + SetCurrentThreadCoreGroup(CoreGroup::Performance); #endif } void SetCurrentThreadToEfficiencyCores() { #if defined(__ANDROID__) - const cpu_set_t& mask = EfficiencyCoreMask(); - if (CPU_COUNT(&mask) == 0) { - return; - } - if (sched_setaffinity(gettid(), sizeof(mask), &mask) != 0) { - LOG_DEBUG(Common, "Could not restrict thread to efficiency cores: {}", GetLastErrorMsg()); + SetCurrentThreadCoreGroup(CoreGroup::Efficiency); +#endif +} + +void RefreshThreadPolicies() { +#if defined(__ANDROID__) + std::scoped_lock topology_lock{g_topology_mutex}; + RefreshTopologyLocked(); + + std::scoped_lock policy_lock{g_policy_mutex}; + for (const auto& policy : Policies()) { + if (policy.has_nice) { + setpriority(PRIO_PROCESS, static_cast(policy.tid), policy.nice_value); + } + ApplyCoreGroupLocked(policy.tid, policy.group); } #endif } diff --git a/src/common/thread.h b/src/common/thread.h index 9e853511c3..24709dfa35 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -102,11 +102,13 @@ enum class ThreadPriority : u32 { enum class ThreadPlacement : u32 { Default = 0, Background = 1, + Efficiency = 2, }; void SetCurrentThreadPriority(ThreadPriority new_priority); void SetCurrentThreadName(const char* name); void SetCurrentThreadToPerformanceCores(); void SetCurrentThreadToEfficiencyCores(); +void RefreshThreadPolicies(); } // namespace Common diff --git a/src/common/thread_worker.h b/src/common/thread_worker.h index 3f440fcdba..8ae010e6bf 100644 --- a/src/common/thread_worker.h +++ b/src/common/thread_worker.h @@ -42,8 +42,10 @@ public: : workers_queued{num_workers}, thread_name{std::move(name)} { const auto lambda = [this, func, placement](std::stop_token stop_token) { Common::SetCurrentThreadName(thread_name.c_str()); - if (placement == ThreadPlacement::Background) { + if (placement != ThreadPlacement::Default) { Common::SetCurrentThreadPriority(ThreadPriority::Low); + } + if (placement == ThreadPlacement::Efficiency) { Common::SetCurrentThreadToEfficiencyCores(); } { 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/hle/service/vi/conductor.cpp b/src/core/hle/service/vi/conductor.cpp index 002c653b8c..d731d55bba 100644 --- a/src/core/hle/service/vi/conductor.cpp +++ b/src/core/hle/service/vi/conductor.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/settings.h" +#include "common/thread.h" #include "core/core.h" #include "core/core_timing.h" #include "core/hle/service/vi/conductor.h" @@ -76,6 +77,7 @@ void Conductor::ProcessVsync() { void Conductor::VsyncThread(std::stop_token token) { Common::SetCurrentThreadName("VSyncThread"); + Common::SetCurrentThreadPriority(Common::ThreadPriority::High); while (!token.stop_requested()) { m_signal.Wait(); diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index 13838794de..e3fd2301c1 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -266,6 +266,7 @@ void PresentManager::WaitPresent() { void PresentManager::PresentThread(std::stop_token token) { Common::SetCurrentThreadName("VulkanPresent"); + Common::SetCurrentThreadPriority(Common::ThreadPriority::High); 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 2c3f067a29..4cd932bafb 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -346,6 +346,7 @@ bool Scheduler::UpdateRescaling(bool is_rescaling) { void Scheduler::WorkerThread(std::stop_token stop_token) { Common::SetCurrentThreadName("VulkanWorker"); + Common::SetCurrentThreadPriority(Common::ThreadPriority::VeryHigh); 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 23d7cc6cae..9817fadf3f 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -532,7 +532,7 @@ private: u64 last_sampler_gc_frame = (std::numeric_limits::max)(); Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {}, - Common::ThreadPlacement::Background}; + 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 058cc0ab6f..9f37dfc48e 100644 --- a/src/video_core/textures/workers.cpp +++ b/src/video_core/textures/workers.cpp @@ -11,7 +11,7 @@ namespace Tegra::Texture { Common::ThreadWorker& GetThreadWorkers() { static Common::ThreadWorker workers{(std::max)(std::thread::hardware_concurrency(), 2U) / 2, "ImageTranscode", {}, - Common::ThreadPlacement::Background}; + Common::ThreadPlacement::Efficiency}; return workers; }