aboutsummaryrefslogtreecommitdiff
path: root/src/common/thread.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/common/thread.cpp')
-rw-r--r--src/common/thread.cpp538
1 files changed, 505 insertions, 33 deletions
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