Merge pull request #3447 from sypher69/fix/thread-process-affinity-inheritance

fix(libkernel): inherit process affinity in new guest threads
This commit is contained in:
Adrià 2026-10-11 13:44:45 +02:00 committed by GitHub
commit 97fbc7e629
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 49 additions and 5 deletions

View File

@ -32,6 +32,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr)
p->_detachstate = thread->_detached ? 1 : 0;
p->_schedpriority = thread->priority.load(std::memory_order_relaxed);
p->_affinity = thread->affinity.load(std::memory_order_relaxed);
p->_affinityExplicit = true;
return 0;
}

View File

@ -99,6 +99,7 @@ struct PthreadAttrPrivate {
int _schedpolicy;
int _inheritsched;
KernelCpumask _affinity = DEFAULT_THREAD_AFFINITY;
bool _affinityExplicit = false;
std::size_t _guardsize = 0x1000;
int _solosched = 0;
};
@ -139,7 +140,7 @@ struct PthreadPrivate {
std::mutex cancelLock;
TimedWait::Condition* cancelWait = nullptr;
PthreadPrivate();
explicit PthreadPrivate(const PthreadAttrPrivate* attr = nullptr);
~PthreadPrivate();
PthreadPrivate(const PthreadPrivate&) = delete;
PthreadPrivate& operator=(const PthreadPrivate&) = delete;

View File

@ -83,6 +83,7 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) {
(*attr)->_schedpolicy = SCHED_FIFO_PS5;
(*attr)->_inheritsched = 4;
(*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed);
(*attr)->_affinityExplicit = true;
return SCE_OK;
}
@ -137,6 +138,7 @@ int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_
int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) {
if (!attr || !*attr) throw std::runtime_error("scePthreadAttrSetaffinity: null attr");
(*attr)->_affinity = mask;
(*attr)->_affinityExplicit = true;
return SCE_OK;
}

View File

@ -186,6 +186,7 @@ struct CpuClockRegistry {
std::mutex lock;
std::map<int, PthreadPrivate*> threads;
int nextThread = 100000;
KernelCpumask processAffinity = DEFAULT_THREAD_AFFINITY;
};
static CpuClockRegistry& CpuClocks() {
@ -193,9 +194,12 @@ static CpuClockRegistry& CpuClocks() {
return *registry;
}
PthreadPrivate::PthreadPrivate() : _finished(false), _retval(nullptr), _detached(false), _adopted(false) {
PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false), _retval(nullptr), _detached(false), _adopted(false) {
auto& registry = CpuClocks();
std::lock_guard lock(registry.lock);
const auto* parent = CurrentGuestState().current;
const KernelCpumask inherited = parent ? parent->affinity.load(std::memory_order_relaxed) : registry.processAffinity;
affinity.store(attr && attr->_affinityExplicit ? attr->_affinity : inherited, std::memory_order_relaxed);
if (registry.nextThread > static_cast<int>(CPU_CLOCK_ID_MASK))
throw std::runtime_error("Guest thread CPU clock identifiers are exhausted");
cpuClockThread = registry.nextThread++;
@ -230,6 +234,7 @@ int GuestThreadCpuClockId(const PthreadPrivate* thread) {
void SetLiveGuestThreadsAffinity(KernelCpumask mask) {
auto& registry = CpuClocks();
std::lock_guard lock(registry.lock);
registry.processAffinity = mask;
for (const auto& [id, thread] : registry.threads) {
if (!thread->_finished.load(std::memory_order_acquire)) thread->affinity.store(mask, std::memory_order_relaxed);
}
@ -517,13 +522,12 @@ extern "C" {
int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name) {
if (!thread || !entry) throw std::runtime_error("scePthreadCreate: null arg");
if (attr && !*attr) throw std::runtime_error("scePthreadCreate: null attributes");
auto p = std::make_unique<PthreadPrivate>();
auto p = std::make_unique<PthreadPrivate>(attr ? *attr : nullptr);
bool detached = false;
if (attr && *attr) detached = ((*attr)->_detachstate == DETACH_DETACHED);
p->_detached = detached;
p->stackSize = attr ? (*attr)->_stacksize : DEFAULT_STACK_SIZE;
if (attr) {
p->affinity.store((*attr)->_affinity, std::memory_order_relaxed);
p->priority.store((*attr)->_schedpriority, std::memory_order_relaxed);
}
if (name) p->name = name;

View File

@ -15,6 +15,7 @@ int APS5_VABI scePthreadSetaffinity(Pthread thread, KernelCpumask mask);
int APS5_VABI scePthreadAttrInit(PthreadAttr* attr);
int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr);
int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask);
int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask);
int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, void* mask);
int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, const void* mask);
int APS5_VABI getpid_nid_postfix(void);
@ -28,6 +29,16 @@ static constexpr int PS5_LOGICAL_CPUS = 16;
static void Require(bool value) { if (!value) std::abort(); }
struct RestoreProcessMask {
KernelCpumask mask;
bool* restored;
~RestoreProcessMask() {
const std::uint64_t requested[1] = {mask};
*restored = cpuset_setaffinity_nid_postfix(3, 2, -1, 8, requested) == 0;
}
};
static constexpr std::size_t GUEST_CPUSET_BYTES = 32;
static constexpr int GUEST_EINVAL = 22;
static constexpr int GUEST_EFAULT = 14;
@ -162,6 +173,31 @@ int main() {
Require(bystanderAffinity == available);
hold.set_value();
Require(scePthreadJoin(bystander, nullptr) == SCE_OK);
if (first != available) {
const std::uint64_t single[1] = {last};
const bool changed = cpuset_setaffinity_nid_postfix(3, 2, -1, 8, single) == 0;
bool restored = false;
bool created = false;
bool joined = false;
bool explicitSet = false;
bool explicitCreated = false;
bool explicitJoined = false;
std::uint64_t inherited[8]{};
std::uint64_t explicitAffinity[8]{};
if (changed) {
RestoreProcessMask restore{available, &restored};
Pthread child = nullptr;
created = scePthreadCreate(&child, &attr, ReportCpuset, inherited, nullptr) == SCE_OK;
joined = created && scePthreadJoin(child, nullptr) == SCE_OK;
explicitSet = scePthreadAttrSetaffinity(&attr, available) == SCE_OK;
child = nullptr;
explicitCreated = explicitSet && scePthreadCreate(&child, &attr, ReportCpuset, explicitAffinity, nullptr) == SCE_OK;
explicitJoined = explicitCreated && scePthreadJoin(child, nullptr) == SCE_OK;
}
Require(changed && restored && created && joined && inherited[0] == 0 && inherited[1] == last);
Require(explicitSet && explicitCreated && explicitJoined && explicitAffinity[0] == 0 && explicitAffinity[1] == available);
}
Require(scePthreadAttrDestroy(&attr) == SCE_OK);
Require(ReadCpuset(pthread_self_nid_postfix()) == available);

View File

@ -401,7 +401,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sigaltstack](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported
- [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`.
- [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls.
- [cpuset_setaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. For the process, the mask is stored on every guest thread that is alive at that moment; FreeBSD gives the whole process one cpuset that later threads join, but here a thread created afterwards still starts with its attribute's mask. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set
- [cpuset_setaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set
- [dup and dup2](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` between a guest socket and a host descriptor in either direction, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates and the descriptor numbers come from the guest socket counter, not the lowest free number as in FreeBSD `kern_dup`. The errno values for descriptor exhaustion are not verified against the console.
- [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior.
- [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws.