diff options
Diffstat (limited to 'src/common/thread.cpp')
| -rw-r--r-- | src/common/thread.cpp | 538 |
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(¤t); + if (sched_getaffinity(getpid(), sizeof(current), ¤t) != 0) { + return; + } + if (std::memcmp(¤t, &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, ¶ms); } - - pthread_setschedparam(this_thread, scheduling_type, ¶ms); #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(¤t); + if (sched_getaffinity(tid, sizeof(current), ¤t) == 0 && + std::memcmp(¤t, &expected, sizeof(current)) == 0) { + return; + } + RefreshThreadPolicies(); +#endif } #ifdef ARCHITECTURE_x86_64 |
