From 2ba8f75e15ece357424501f01f13e6d43991d61b Mon Sep 17 00:00:00 2001 From: clem-gb Date: Thu, 8 Oct 2026 09:45:04 +0200 Subject: [PATCH] fix(libkernel): report the current CPU within the thread's guest affinity sceKernelGetCurrentCpu returned the raw host CPU index. Guest affinities are bookkeeping only and sceKernelGetAvailableCpumask advertises CPUs 0 to 12, so on a host with more than 13 logical CPUs a title could get 13 or higher, and a thread pinned to one CPU could get another one. The host index now selects a CPU of the calling thread's affinity clipped to the available mask, so the result is always an advertised CPU and a thread pinned to one CPU reports that CPU. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../prx/libkernel/Pthread/include/Pthread.hpp | 8 +++ .../libs/prx/libkernel/System/src/Process.cpp | 7 +- core/libs/tests/GuestCurrentCpu.cpp | 66 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestCurrentCpu.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index daa45a8c7..763cb3985 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -327,6 +327,16 @@ if(BUILD_TESTING) set_tests_properties(guest_available_cpumask PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_current_cpu_tests ${LIBS_INCLUDE_DIR}/tests/GuestCurrentCpu.cpp) + target_include_directories(guest_current_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_current_cpu_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_current_cpu_tests) + add_test(NAME guest_current_cpu COMMAND guest_current_cpu_tests) + set_tests_properties(guest_current_cpu PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_current_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_getrusage_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetrusage.cpp) target_include_directories(guest_getrusage_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_getrusage_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 2f3fb16eb..f9db57ed3 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -5,6 +5,7 @@ #include "SceTypes.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include +#include #include #include #include @@ -63,6 +64,13 @@ struct PthreadSemPrivate { static constexpr KernelCpumask DEFAULT_THREAD_AFFINITY = 0x1FFF; static constexpr int DEFAULT_THREAD_PRIORITY = 700; +inline int GuestCpuFromHost(unsigned hostCpu, KernelCpumask threadAffinity) { + KernelCpumask mask = threadAffinity & DEFAULT_THREAD_AFFINITY; + if (mask == 0) mask = DEFAULT_THREAD_AFFINITY; + for (auto skip = hostCpu % static_cast(std::popcount(mask)); skip != 0; --skip) mask &= mask - 1; + return std::countr_zero(mask); +} + struct PthreadAttrPrivate { void* stackAddress = nullptr; std::size_t _stacksize; diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index cc9e1301f..7c0f3e984 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -13,6 +13,7 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include #include @@ -143,6 +144,8 @@ struct GuestResourceUsage { std::int64_t ru_nivcsw; }; +extern "C" Pthread APS5_VABI scePthreadSelf(); + extern "C" { // unknown data @@ -190,13 +193,13 @@ int APS5_VABI sceKernelGetCurrentCpu(void) { throw std::system_error(GetLastError(), std::system_category(), "Reading processor group size"); index += count; } - return static_cast(index); #else const int cpu = ::sched_getcpu(); if (cpu < 0) throw std::system_error(errno, std::generic_category(), "Reading current processor"); - return cpu; + const auto index = static_cast(cpu); #endif + return GuestCpuFromHost(index, scePthreadSelf()->affinity.load(std::memory_order_relaxed)); } std::uint64_t APS5_VABI sceKernelGetGPI(void) { diff --git a/core/libs/tests/GuestCurrentCpu.cpp b/core/libs/tests/GuestCurrentCpu.cpp new file mode 100644 index 000000000..b7d7d75cc --- /dev/null +++ b/core/libs/tests/GuestCurrentCpu.cpp @@ -0,0 +1,66 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelGetCurrentCpu(void); +KernelCpumask APS5_VABI sceKernelGetAvailableCpumask(void); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadSetaffinity(Pthread thread, KernelCpumask mask); +} + +static constexpr int SCE_OK = 0; +static constexpr unsigned MAX_HOST_CPUS = 256; +static constexpr int CALLS_PER_THREAD = 64; + +static void Require(bool value) { if (!value) std::abort(); } + +static bool InMask(int cpu, KernelCpumask mask) { + return cpu >= 0 && cpu < 64 && (mask >> cpu & 1) != 0; +} + +struct PinnedRun { + KernelCpumask affinity; + bool inside; +}; + +static void* APS5_VABI ReportWhilePinned(void* arg) { + auto* run = static_cast(arg); + run->inside = scePthreadSetaffinity(scePthreadSelf(), run->affinity) == SCE_OK; + for (int call = 0; call < CALLS_PER_THREAD && run->inside; ++call) + run->inside = InMask(sceKernelGetCurrentCpu(), run->affinity); + return nullptr; +} + +int main() { + const KernelCpumask available = sceKernelGetAvailableCpumask(); + + const std::array affinities{available, 0x1FFB, 0x3, 0x3F, 0x1000, 0x1, 0xFFFF, 0x10000}; + for (const KernelCpumask affinity : affinities) { + const KernelCpumask expected = (affinity & available) != 0 ? affinity & available : available; + KernelCpumask reached = 0; + for (unsigned hostCpu = 0; hostCpu < MAX_HOST_CPUS; ++hostCpu) { + const int cpu = GuestCpuFromHost(hostCpu, affinity); + Require(InMask(cpu, expected)); + Require(GuestCpuFromHost(hostCpu, affinity) == cpu); + reached |= KernelCpumask{1} << cpu; + if (hostCpu + 1 == static_cast(std::popcount(expected))) Require(reached == expected); + } + Require(reached == expected); + } + + for (int call = 0; call < CALLS_PER_THREAD; ++call) Require(InMask(sceKernelGetCurrentCpu(), available)); + + for (KernelCpumask cpu = 1; cpu != 0; cpu <<= 1) { + if ((available & cpu) == 0) continue; + PinnedRun run{cpu, false}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, ReportWhilePinned, &run, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(run.inside); + } +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6e8898abd..7d060bd94 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -116,6 +116,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed +- [sceKernelGetCurrentCpu](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is the SDK 9 `libc.prx` heap (Astro Bot PPSA21564, PPSA22520), which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0 - [sceKernelMtypeprotect](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Guest arena](../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws