feat(libSceJobManager): implement CPU job sequences

This commit is contained in:
fma-rc 2026-10-11 01:50:03 +02:00 committed by Adrià Franch
parent 90f26c996c
commit e2a7aac32d
4 changed files with 981 additions and 0 deletions

View File

@ -0,0 +1,18 @@
add_library(libSceJobManager SHARED EXCLUDE_FROM_ALL Export.cpp)
target_include_directories(libSceJobManager PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(libSceJobManager)
if(BUILD_TESTING)
add_test_executable(guest_job_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp)
target_include_directories(guest_job_manager_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libc)
configure_windows_unwind(guest_job_manager_tests)
foreach(testCase IN ITEMS memory validation dispatch ordering limits priority shutdown)
add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase})
set_tests_properties(guest_job_manager_${testCase} PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_job_manager_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceJobManager>")
endif()
endforeach()
endif()

View File

@ -0,0 +1,578 @@
#include <array>
#include <bit>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <deque>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI scePthreadAttrInit(PthreadAttr* attr);
int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr);
int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched);
int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param);
int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize);
int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask);
int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name);
int APS5_VABI scePthreadJoin(Pthread thread, void** result);
}
namespace {
constexpr int ManagerInvalidArgument = static_cast<int>(0x81581000u);
constexpr int ManagerNullMemory = static_cast<int>(0x81581001u);
constexpr int ManagerMemoryTooSmall = static_cast<int>(0x81581002u);
constexpr int ManagerNotInitialized = static_cast<int>(0x81581010u);
constexpr int ManagerAlreadyInitialized = static_cast<int>(0x81581020u);
constexpr int ManagerMisalignedMemory = static_cast<int>(0x81581030u);
constexpr int SequenceOutOfRange = static_cast<int>(0x81580000u);
constexpr int SequenceNullArgument = static_cast<int>(0x81580001u);
constexpr int SequenceNoNodes = static_cast<int>(0x81580002u);
constexpr int SequenceLimitExceeded = static_cast<int>(0x81580003u);
constexpr int SequenceNoneFree = static_cast<int>(0x81580006u);
constexpr int SequenceInvalid = static_cast<int>(0x81580007u);
constexpr std::uint32_t MaxWorkers = 13;
constexpr std::uint32_t MaxSequences = 32;
constexpr std::uint32_t MaxInvocations = 0x7fff;
constexpr std::uint32_t MaxDispatches = 0x3ff;
constexpr std::uint32_t MaxSyncPoints = 0xff;
constexpr std::uint32_t MaxWaits = 0x3f;
constexpr std::uint64_t StateOffset = 0x10;
constexpr std::uint64_t StateEnd = 0x2d8;
constexpr std::uint64_t ThreadRecordBytes = 0x48;
constexpr std::uint64_t SequenceBytes = 0x240;
constexpr std::uint64_t NodeBytes = 72;
struct MemorySizeQueryParams {
std::uint32_t version;
std::uint32_t workers;
std::uint32_t sequences;
std::uint32_t invocations;
std::uint32_t dispatches;
std::uint32_t syncPoints;
std::uint32_t waits;
std::uint32_t computeSequences;
std::uint64_t computeQueueMask;
};
struct ConfigParams {
std::uint32_t version;
std::uint32_t threadFlags;
std::uint32_t stackSize;
std::uint32_t priority;
const char* threadName;
std::uint32_t spinMicroseconds;
std::uint32_t computeAffinity;
std::uint32_t computePriority;
std::uint32_t reserved;
};
constexpr ConfigParams DefaultConfig{0, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x1fff, 0x100, 0};
struct BlockHeader {
std::uint16_t magic;
std::uint16_t stateOffset;
std::uint32_t version;
std::uint32_t size;
std::uint32_t reserved;
};
using JobEntry = void (APS5_VABI*)(void* argument, std::uint32_t index);
std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) {
return (value + alignment - 1) & ~(alignment - 1);
}
struct Manager;
class Sequence;
enum class ItemType { Dispatch, SyncPoint, Wait };
struct Item {
ItemType type;
Sequence* sequence;
JobEntry entry;
void* argument;
std::uint32_t count;
std::uint32_t handedOut;
std::uint32_t completed;
bool* passed;
};
struct Counters {
std::uint32_t invocations = 0;
std::uint32_t dispatches = 0;
std::uint32_t syncPoints = 0;
std::uint32_t waits = 0;
};
class SequenceInterface {
public:
APS5_VABI virtual int Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char* name) = 0;
APS5_VABI virtual int SyncPoint() = 0;
APS5_VABI virtual int Wait() = 0;
APS5_VABI virtual ~SequenceInterface() = default;
APS5_VABI virtual int DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) = 0;
APS5_VABI virtual int DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) = 0;
static void operator delete(void*) { throw std::runtime_error("libSceJobManager: a sequence was deleted by its caller"); }
};
class SequenceFactoryInterface {
public:
APS5_VABI virtual int Create(SequenceInterface** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) = 0;
APS5_VABI virtual int Destroy(SequenceInterface* sequence) = 0;
APS5_VABI virtual ~SequenceFactoryInterface() = default;
APS5_VABI virtual int CreateCompute(SequenceInterface** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) = 0;
APS5_VABI virtual int DestroyCompute(SequenceInterface* sequence) = 0;
static void operator delete(void*) { throw std::runtime_error("libSceJobManager: a sequence factory was deleted by its caller"); }
};
class Sequence final : public SequenceInterface {
public:
APS5_VABI int Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char* name) override;
APS5_VABI int SyncPoint() override;
APS5_VABI int Wait() override;
APS5_VABI int DispatchCompute(const void*, void*, std::int32_t, std::int32_t, std::int32_t, const char*) override {
NotImplemented_nid_no_patch("sce::Job compute dispatch");
return 0;
}
APS5_VABI int DispatchComputeIndirect(const void*, void*, const void*, const char*) override {
NotImplemented_nid_no_patch("sce::Job indirect compute dispatch");
return 0;
}
bool Open(Manager* owner, const Counters& limit, std::uint32_t runQueue);
void Close();
void Advance();
void DispatchCompleted(std::uint32_t invocations);
Manager* manager = nullptr;
bool open = false;
private:
int Latch(int code);
Counters limits;
Counters submitted;
Counters completed;
std::uint32_t priority = 0;
int error = 0;
std::uint32_t outstanding = 0;
std::size_t cursor = 0;
std::deque<Item> items;
std::mutex waiters;
std::condition_variable passed;
};
class Factory final : public SequenceFactoryInterface {
public:
APS5_VABI int Create(SequenceInterface** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) override;
APS5_VABI int Destroy(SequenceInterface* sequence) override;
APS5_VABI int CreateCompute(SequenceInterface** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char* name) override;
APS5_VABI int DestroyCompute(SequenceInterface* sequence) override;
Manager* manager = nullptr;
std::uint32_t priority = 0;
};
struct Manager {
std::mutex lock;
std::condition_variable work;
std::condition_variable nodeFreed;
bool running = false;
std::uint32_t blockedWaiters = 0;
std::uint32_t freeNodes = 0;
std::uint32_t freeSequences = 0;
std::uint32_t persistentThreads = 0;
std::array<std::deque<Item*>, 2> runQueues;
std::vector<Sequence> sequences;
std::array<Factory, 2> factories;
std::array<Factory, 2> unifiedFactories;
std::vector<Pthread> workers;
};
Manager*& Implementation(void* self) {
return *static_cast<Manager**>(self);
}
bool TakeNode(Manager& manager) {
if (manager.freeNodes == 0) return false;
--manager.freeNodes;
return true;
}
void ReturnNodes(Manager& manager, std::uint32_t count) {
manager.freeNodes += count;
manager.nodeFreed.notify_all();
}
int Sequence::Latch(int code) {
if (error == 0) error = code;
return code;
}
bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueue) {
manager = owner;
limits = limit;
submitted = {};
completed = {};
priority = runQueue;
error = 0;
outstanding = 0;
cursor = 0;
items.clear();
if (!TakeNode(*owner)) return false;
open = true;
return true;
}
void Sequence::Close() {
if (!open) return;
ReturnNodes(*manager, static_cast<std::uint32_t>(items.size()) + 1);
items.clear();
cursor = 0;
outstanding = 0;
open = false;
}
void Sequence::Advance() {
while (cursor < items.size()) {
Item& item = items[cursor];
if (item.type == ItemType::Dispatch) {
++outstanding;
++cursor;
manager->runQueues[priority].push_back(&item);
manager->work.notify_all();
continue;
}
if (outstanding != 0) return;
if (item.type == ItemType::SyncPoint) {
++completed.syncPoints;
++cursor;
continue;
}
bool* const waiter = item.passed;
const auto retired = static_cast<std::uint32_t>(cursor) + 1;
items.erase(items.begin(), items.begin() + static_cast<std::ptrdiff_t>(retired));
cursor = 0;
if (error == 0) {
submitted.invocations -= completed.invocations;
submitted.dispatches -= completed.dispatches;
submitted.syncPoints -= completed.syncPoints;
--submitted.waits;
completed = {};
}
ReturnNodes(*manager, retired);
*waiter = true;
passed.notify_all();
}
}
void Sequence::DispatchCompleted(std::uint32_t invocations) {
completed.invocations += invocations;
++completed.dispatches;
--outstanding;
if (outstanding == 0) Advance();
}
int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, const char*) {
if (entry == nullptr) return SequenceNullArgument;
if (count == 0 || count > MaxInvocations) return SequenceOutOfRange;
const std::lock_guard guard(manager->lock);
if (error != 0) return SequenceInvalid;
const Counters before = submitted;
submitted.invocations += count;
++submitted.dispatches;
if (before.invocations + count > limits.invocations || before.dispatches >= limits.dispatches) return Latch(SequenceLimitExceeded);
if (!TakeNode(*manager)) return Latch(SequenceNoNodes);
items.push_back({ItemType::Dispatch, this, entry, argument, count, 0, 0, nullptr});
Advance();
return 0;
}
int Sequence::SyncPoint() {
const std::lock_guard guard(manager->lock);
if (error != 0) return SequenceInvalid;
const std::uint32_t before = submitted.syncPoints++;
if (before >= limits.syncPoints) return Latch(SequenceLimitExceeded);
if (!TakeNode(*manager)) return Latch(SequenceNoNodes);
items.push_back({ItemType::SyncPoint, this, nullptr, nullptr, 0, 0, 0, nullptr});
Advance();
return 0;
}
int Sequence::Wait() {
const std::lock_guard serial(waiters);
std::unique_lock guard(manager->lock);
int result = 0;
if (error != 0) {
result = SequenceInvalid;
} else if (submitted.waits++ >= limits.waits) {
result = Latch(SequenceLimitExceeded);
}
if (manager->freeNodes == 0 && result == 0) result = Latch(SequenceNoNodes);
++manager->blockedWaiters;
manager->nodeFreed.wait(guard, [this] { return manager->freeNodes != 0; });
--manager->freeNodes;
bool done = false;
items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done});
Advance();
passed.wait(guard, [&done] { return done; });
--manager->blockedWaiters;
return result;
}
bool ValidLimits(std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits) {
if (invocations > MaxInvocations || dispatches > MaxDispatches || syncPoints > MaxSyncPoints || waits > MaxWaits) return false;
return invocations != 0 || dispatches != 0 || syncPoints != 0 || waits != 0;
}
int Factory::Create(SequenceInterface** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) {
if (out == nullptr) return SequenceNullArgument;
if (!ValidLimits(invocations, dispatches, syncPoints, waits)) return SequenceOutOfRange;
const std::lock_guard guard(manager->lock);
if (manager->freeSequences == 0) return SequenceNoneFree;
const int index = std::countr_zero(manager->freeSequences);
manager->freeSequences &= ~(std::uint32_t{1} << index);
Sequence& sequence = manager->sequences[static_cast<std::size_t>(index)];
if (!sequence.Open(manager, {invocations, dispatches, syncPoints, waits}, priority)) return SequenceNoNodes;
*out = &sequence;
return 0;
}
int Factory::Destroy(SequenceInterface* sequence) {
Sequence* found = nullptr;
std::uint32_t index = 0;
{
const std::lock_guard guard(manager->lock);
for (; index < manager->sequences.size(); ++index) {
if (&manager->sequences[index] == sequence) break;
}
if (index == manager->sequences.size() || (manager->freeSequences & (std::uint32_t{1} << index)) != 0) return SequenceInvalid;
found = &manager->sequences[index];
}
static_cast<void>(found->Wait());
const std::lock_guard guard(manager->lock);
found->Close();
manager->freeSequences |= std::uint32_t{1} << index;
return 0;
}
int Factory::CreateCompute(SequenceInterface** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) {
if (out == nullptr) return SequenceNullArgument;
if (!ValidLimits(invocations, dispatches, syncPoints, waits)) return SequenceOutOfRange;
return SequenceNoneFree;
}
int Factory::DestroyCompute(SequenceInterface*) {
return SequenceInvalid;
}
bool RunOne(Manager& manager, std::unique_lock<std::mutex>& guard) {
Item* item = nullptr;
for (const std::size_t queue : {std::size_t{1}, std::size_t{0}}) {
auto& pending = manager.runQueues[queue];
if (pending.empty()) continue;
item = pending.front();
if (item->handedOut + 1 == item->count) pending.pop_front();
break;
}
if (item == nullptr) return false;
const std::uint32_t index = item->handedOut++;
const JobEntry entry = item->entry;
void* const argument = item->argument;
guard.unlock();
entry(argument, index);
guard.lock();
if (++item->completed == item->count) item->sequence->DispatchCompleted(item->count);
return true;
}
void* APS5_VABI WorkerEntry(void* argument) {
Manager& manager = *static_cast<Manager*>(argument);
std::unique_lock guard(manager.lock);
while (manager.running) {
if (RunOne(manager, guard)) continue;
manager.work.wait(guard, [&manager] { return !manager.running || !manager.runQueues[0].empty() || !manager.runQueues[1].empty(); });
}
return nullptr;
}
std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, bool* fits) {
const std::uint64_t end = memory + size;
std::uint64_t address = AlignUp(memory + StateEnd, 8) + workers * ThreadRecordBytes;
for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes;
address = AlignUp(AlignUp(address, 64) + sequences * SequenceBytes, 64);
*fits = address <= end;
return *fits ? static_cast<std::uint32_t>(((end - address) / NodeBytes) & 0xffff) : 0;
}
int ValidateWorkers(const ConfigParams& config, std::uint32_t workers) {
const std::uint32_t mode = config.threadFlags & 0x30000;
if (config.reserved != 0 || config.priority < 0x100 || config.priority > 0x2ff || config.stackSize < 0x4000) return ManagerInvalidArgument;
if ((config.threadFlags & 0xfffce000) != 0 || static_cast<std::uint32_t>(std::popcount(config.threadFlags & 0x1fff)) < workers) return ManagerInvalidArgument;
if ((mode != 0x10000 && mode != 0x20000) || config.spinMicroseconds > 0x32) return ManagerInvalidArgument;
return 0;
}
void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t workers) {
std::uint32_t remaining = config.threadFlags & 0x1fff;
for (std::uint32_t index = 0; index < workers; ++index) {
KernelCpumask affinity = config.threadFlags & 0x1fff;
if ((config.threadFlags & 0x30000) == 0x10000) {
affinity = remaining & (~remaining + 1);
remaining &= remaining - 1;
}
const std::string name = config.threadName != nullptr ? std::string(config.threadName).substr(0, 31) : "SceJobManagerWorker" + std::to_string(index);
const KernelSchedParam schedule{static_cast<int>(config.priority)};
PthreadAttr attributes = nullptr;
if (scePthreadAttrInit(&attributes) != 0) throw std::runtime_error("libSceJobManager: scePthreadAttrInit failed");
scePthreadAttrSetinheritsched(&attributes, 0);
scePthreadAttrSetschedparam(&attributes, &schedule);
scePthreadAttrSetstacksize(&attributes, config.stackSize);
scePthreadAttrSetaffinity(&attributes, affinity);
Pthread thread = nullptr;
const int created = scePthreadCreate(&thread, &attributes, WorkerEntry, &manager, name.c_str());
scePthreadAttrDestroy(&attributes);
if (created != 0) throw std::runtime_error("libSceJobManager: scePthreadCreate failed for a worker");
manager.workers.push_back(thread);
}
}
int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeSequences, std::uint64_t computeQueueMask, const ConfigParams* config, bool unified) {
if (Implementation(self) != nullptr) return ManagerAlreadyInitialized;
if (memory == nullptr) return ManagerNullMemory;
const auto base = reinterpret_cast<std::uint64_t>(memory);
if ((base & 0xff) != 0) return ManagerMisalignedMemory;
if (size == 0 || workers == 0 || workers > MaxWorkers) return ManagerInvalidArgument;
if (unified) {
if ((sequences == 0 && computeSequences == 0) || sequences + computeSequences > MaxSequences || (computeQueueMask >> 56) != 0 || static_cast<std::uint32_t>(std::popcount(computeQueueMask)) < computeSequences) return ManagerInvalidArgument;
} else if (sequences == 0 || sequences > MaxSequences) {
return ManagerInvalidArgument;
}
if (size < StateOffset) return ManagerMemoryTooSmall;
if (size < StateEnd) throw std::runtime_error("sce::Job::JobManager::initialize: a memory block smaller than 0x2d8 bytes is not supported");
std::memset(static_cast<std::uint8_t*>(memory) + StateOffset, 0, StateEnd - StateOffset);
const BlockHeader header{0x4d4a, static_cast<std::uint16_t>(StateOffset), 0x05000001, static_cast<std::uint32_t>(size), 0};
std::memcpy(memory, &header, sizeof(header));
if (config != nullptr && config->version > 1) return ManagerInvalidArgument;
if (computeSequences != 0) NotImplemented_nid_no_patch("sce::Job::JobManager::initialize with compute sequences");
const ConfigParams& settings = config != nullptr ? *config : DefaultConfig;
bool fits = false;
const std::uint32_t nodes = NodeCount(base, size, workers, sequences, &fits);
if (!fits || nodes == 0) return ManagerMemoryTooSmall;
if (const int invalid = ValidateWorkers(settings, workers); invalid != 0) return invalid;
auto* manager = new Manager;
manager->running = true;
manager->freeNodes = nodes - 1;
manager->freeSequences = sequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << sequences) - 1;
manager->sequences = std::vector<Sequence>(sequences);
for (std::uint32_t priority = 0; priority < 2; ++priority) {
manager->factories[priority].manager = manager;
manager->factories[priority].priority = priority;
manager->unifiedFactories[priority].manager = manager;
manager->unifiedFactories[priority].priority = priority;
}
Implementation(self) = manager;
StartWorkers(*manager, settings, workers);
return 0;
}
SequenceFactoryInterface* FactoryFor(void* self, std::uint32_t priority, bool unified) {
Manager* manager = Implementation(self);
if (manager == nullptr) return nullptr;
if (priority > 1) throw std::runtime_error("sce::Job::JobManager: sequence factory priority " + std::to_string(priority) + " does not exist");
return unified ? &manager->unifiedFactories[priority] : &manager->factories[priority];
}
}
extern "C" {
void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void* self) {
Implementation(self) = nullptr;
}
void APS5_VABI _ZN3sce3Job10JobManagerC2Ev(void* self) {
Implementation(self) = nullptr;
}
void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*) {
}
void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void*) {
}
std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams* params) {
if (params == nullptr || params->version > 1 || params->workers == 0 || params->workers > MaxWorkers || params->sequences > MaxSequences) return 0;
const bool unified = params->version == 1;
const std::uint32_t computeSequences = unified ? params->computeSequences : 0;
if (unified) {
if (computeSequences > MaxSequences || params->sequences + computeSequences == 0 || params->sequences + computeSequences > MaxSequences) return 0;
if ((params->computeQueueMask >> 56) != 0 || static_cast<std::uint32_t>(std::popcount(params->computeQueueMask)) < computeSequences) return 0;
} else if (params->sequences == 0) {
return 0;
}
const std::uint64_t total = params->sequences + computeSequences;
if (params->invocations > total * MaxInvocations || params->dispatches > total * MaxDispatches || params->syncPoints > total * MaxSyncPoints || params->waits > total * MaxWaits) return 0;
if (computeSequences != 0) NotImplemented_nid_no_patch("sce::Job::JobManager::calculateRequiredMemorySize with compute sequences");
std::uint64_t bytes = (NodeBytes * params->workers + 0x317) & 0xfc0;
bytes = (bytes + 0x13f) & ~std::uint64_t{0x3f};
bytes = (bytes + 0x0bf) & ~std::uint64_t{0x3f};
bytes = (bytes + 0x13f) & ~std::uint64_t{0x3f};
bytes += params->sequences * SequenceBytes;
const std::uint32_t nodes = params->dispatches + params->syncPoints + params->waits + static_cast<std::uint32_t>(total) + 1;
return AlignUp(bytes, 64) + std::uint64_t{nodes} * NodeBytes + 0x800;
}
int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void* self, void* memory, std::uint64_t memorySize, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeSequences, std::uint64_t computeQueueMask, const ConfigParams* config) {
return Initialize(self, memory, memorySize, workers, sequences, computeSequences, computeQueueMask, config, true);
}
int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE(void* self, void* memory, std::uint64_t memorySize, std::uint32_t workers, std::uint32_t sequences, const ConfigParams* config) {
return Initialize(self, memory, memorySize, workers, sequences, 0, 0, config, false);
}
int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) {
Manager* manager = Implementation(self);
if (manager == nullptr) return ManagerNotInitialized;
{
const std::lock_guard guard(manager->lock);
if (manager->blockedWaiters != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: a thread is still blocked in a sequence wait");
manager->running = false;
manager->work.notify_all();
}
for (const Pthread worker : manager->workers) {
if (scePthreadJoin(worker, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed");
}
Implementation(self) = nullptr;
delete manager;
return 0;
}
SequenceFactoryInterface* APS5_VABI _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) {
return FactoryFor(self, priority, false);
}
SequenceFactoryInterface* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void* self, std::uint32_t priority) {
return FactoryFor(self, priority, true);
}
int APS5_VABI _ZN3sce3Job10JobManager24setPersistentThreadCountEj(void* self, std::uint32_t count) {
Manager* manager = Implementation(self);
if (manager == nullptr) return ManagerNotInitialized;
if (count > manager->workers.size()) return ManagerInvalidArgument;
const std::lock_guard guard(manager->lock);
manager->persistentThreads = count;
return 0;
}
}

View File

@ -0,0 +1,384 @@
#include "SceTypes.hpp"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#include <vector>
struct MemorySizeQueryParams {
std::uint32_t version;
std::uint32_t workers;
std::uint32_t sequences;
std::uint32_t invocations;
std::uint32_t dispatches;
std::uint32_t syncPoints;
std::uint32_t waits;
std::uint32_t computeSequences;
std::uint64_t computeQueueMask;
};
struct ConfigParams {
std::uint32_t version;
std::uint32_t threadFlags;
std::uint32_t stackSize;
std::uint32_t priority;
const char* threadName;
std::uint32_t spinMicroseconds;
std::uint32_t computeAffinity;
std::uint32_t computePriority;
std::uint32_t reserved;
};
using JobEntry = void (APS5_VABI*)(void*, std::uint32_t);
struct Sequence {
APS5_VABI virtual int Dispatch(JobEntry, void*, std::uint32_t, const char*) = 0;
APS5_VABI virtual int SyncPoint() = 0;
APS5_VABI virtual int Wait() = 0;
APS5_VABI virtual ~Sequence() = default;
};
struct SequenceFactory {
APS5_VABI virtual int Create(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0;
APS5_VABI virtual int Destroy(Sequence*) = 0;
APS5_VABI virtual ~SequenceFactory() = default;
APS5_VABI virtual int CreateCompute(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0;
APS5_VABI virtual int DestroyCompute(Sequence*) = 0;
};
extern "C" {
void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*);
void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*);
std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*);
int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, const ConfigParams*);
int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, const ConfigParams*);
int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void*);
SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t);
SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t);
int APS5_VABI _ZN3sce3Job10JobManager24setPersistentThreadCountEj(void*, std::uint32_t);
}
#define Construct _ZN3sce3Job10JobManagerC1Ev
#define Destruct _ZN3sce3Job10JobManagerD1Ev
#define RequiredSize _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE
#define InitializeUnified _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE
#define Initialize _ZN3sce3Job10JobManager10initializeEPvmjjPKNS1_12ConfigParamsE
#define Shutdown _ZN3sce3Job10JobManager8shutdownEv
#define Factories _ZN3sce3Job10JobManager27getSequenceFactoryInterfaceENS1_8PriorityE
#define UnifiedFactories _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE
#define SetPersistentThreadCount _ZN3sce3Job10JobManager24setPersistentThreadCountEj
static constexpr int InvalidArgument = static_cast<int>(0x81581000u);
static constexpr int NullMemory = static_cast<int>(0x81581001u);
static constexpr int MemoryTooSmall = static_cast<int>(0x81581002u);
static constexpr int NotInitialized = static_cast<int>(0x81581010u);
static constexpr int AlreadyInitialized = static_cast<int>(0x81581020u);
static constexpr int MisalignedMemory = static_cast<int>(0x81581030u);
static constexpr int OutOfRange = static_cast<int>(0x81580000u);
static constexpr int NullArgument = static_cast<int>(0x81580001u);
static constexpr int NoNodes = static_cast<int>(0x81580002u);
static constexpr int LimitExceeded = static_cast<int>(0x81580003u);
static constexpr int NoneFree = static_cast<int>(0x81580006u);
static constexpr int Invalid = static_cast<int>(0x81580007u);
static void Check(bool value, int line) {
if (!value) {
std::fprintf(stderr, "job manager check failed at line %d\n", line);
std::abort();
}
}
#define Require(value) Check((value), __LINE__)
struct Block {
explicit Block(std::uint64_t size) : bytes(size), memory(std::aligned_alloc(256, (size + 255) & ~std::uint64_t{255})) {}
~Block() { std::free(memory); }
std::uint64_t bytes;
void* memory;
};
struct ManagerObject {
ManagerObject() { Construct(storage); }
~ManagerObject() { Destruct(storage); }
alignas(64) unsigned char storage[0x100]{};
};
static std::uint64_t SizeFor(std::uint32_t workers, std::uint32_t sequences, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits) {
const MemorySizeQueryParams query{0, workers, sequences, 0, dispatches, syncPoints, waits, 0, 0};
return RequiredSize(&query);
}
static void MemoryCase() {
const MemorySizeQueryParams query{0, 4, 2, 100, 8, 4, 2, 0, 0};
Require(RequiredSize(&query) == 0x17c8);
const MemorySizeQueryParams unified{1, 4, 2, 100, 8, 4, 2, 0, 0};
Require(RequiredSize(&unified) == 0x17c8);
const MemorySizeQueryParams single{0, 1, 1, 0, 0, 0, 0, 0, 0};
Require(RequiredSize(&single) == 0x1090);
Require(RequiredSize(nullptr) == 0);
for (const MemorySizeQueryParams& bad : {
MemorySizeQueryParams{2, 4, 2, 0, 0, 0, 0, 0, 0}, MemorySizeQueryParams{0, 0, 2, 0, 0, 0, 0, 0, 0},
MemorySizeQueryParams{0, 14, 2, 0, 0, 0, 0, 0, 0}, MemorySizeQueryParams{0, 4, 0, 0, 0, 0, 0, 0, 0},
MemorySizeQueryParams{0, 4, 33, 0, 0, 0, 0, 0, 0}, MemorySizeQueryParams{0, 4, 2, 2 * 0x7fff + 1, 0, 0, 0, 0, 0},
MemorySizeQueryParams{0, 4, 2, 0, 2 * 0x3ff + 1, 0, 0, 0, 0}, MemorySizeQueryParams{0, 4, 2, 0, 0, 2 * 0xff + 1, 0, 0, 0},
MemorySizeQueryParams{0, 4, 2, 0, 0, 0, 2 * 0x3f + 1, 0, 0}, MemorySizeQueryParams{1, 4, 0, 0, 0, 0, 0, 0, 0},
MemorySizeQueryParams{1, 4, 2, 0, 0, 0, 0, 0, std::uint64_t{1} << 56}}) {
Require(RequiredSize(&bad) == 0);
}
}
static void APS5_VABI Nothing(void*, std::uint32_t) {
}
static void ValidationCase() {
Block block(SizeFor(2, 2, 16, 4, 4));
ManagerObject manager;
Require(Shutdown(manager.storage) == NotInitialized);
Require(SetPersistentThreadCount(manager.storage, 0) == NotInitialized);
Require(Factories(manager.storage, 0) == nullptr && UnifiedFactories(manager.storage, 1) == nullptr);
Require(Initialize(manager.storage, nullptr, block.bytes, 2, 2, nullptr) == NullMemory);
Require(Initialize(manager.storage, static_cast<char*>(block.memory) + 16, block.bytes - 16, 2, 2, nullptr) == MisalignedMemory);
Require(Initialize(manager.storage, block.memory, 0, 2, 2, nullptr) == InvalidArgument);
Require(Initialize(manager.storage, block.memory, block.bytes, 0, 2, nullptr) == InvalidArgument);
Require(Initialize(manager.storage, block.memory, block.bytes, 14, 2, nullptr) == InvalidArgument);
Require(Initialize(manager.storage, block.memory, block.bytes, 2, 0, nullptr) == InvalidArgument);
Require(Initialize(manager.storage, block.memory, block.bytes, 2, 33, nullptr) == InvalidArgument);
Require(InitializeUnified(manager.storage, block.memory, block.bytes, 2, 0, 0, 0, nullptr) == InvalidArgument);
Require(InitializeUnified(manager.storage, block.memory, block.bytes, 2, 2, 0, std::uint64_t{1} << 56, nullptr) == InvalidArgument);
Require(Initialize(manager.storage, block.memory, 8, 2, 2, nullptr) == MemoryTooSmall);
Require(Initialize(manager.storage, block.memory, 0x400, 2, 2, nullptr) == MemoryTooSmall);
ConfigParams config{0, 0x00021fff, 0x10000, 0x2bc, nullptr, 0, 0x1fff, 0x100, 0};
for (int field = 0; field < 7; ++field) {
ConfigParams bad = config;
if (field == 0) bad.version = 2;
if (field == 1) bad.threadFlags = 0x00001fff;
if (field == 2) bad.threadFlags = 0x00020001;
if (field == 3) bad.stackSize = 0x3fff;
if (field == 4) bad.priority = 0x300;
if (field == 5) bad.spinMicroseconds = 0x33;
if (field == 6) bad.reserved = 1;
Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, &bad) == InvalidArgument);
Require(Factories(manager.storage, 0) == nullptr);
}
config.threadFlags = 0x00010003;
config.threadName = "JobManagerTestWorker";
Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, &config) == 0);
std::uint16_t magic = 0;
std::memcpy(&magic, block.memory, sizeof(magic));
Require(magic == 0x4d4a);
Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, nullptr) == AlreadyInitialized);
Require(SetPersistentThreadCount(manager.storage, 3) == InvalidArgument);
Require(SetPersistentThreadCount(manager.storage, 2) == 0);
SequenceFactory* factory = Factories(manager.storage, 0);
Require(factory != nullptr && Factories(manager.storage, 1) != nullptr && factory != Factories(manager.storage, 1));
Require(UnifiedFactories(manager.storage, 0) != nullptr);
Sequence* sequence = nullptr;
Require(factory->Create(nullptr, 1, 1, 1, 1, nullptr) == NullArgument);
Require(factory->Create(&sequence, 0, 0, 0, 0, nullptr) == OutOfRange);
Require(factory->Create(&sequence, 0x8000, 1, 1, 1, nullptr) == OutOfRange);
Require(factory->Create(&sequence, 1, 0x400, 1, 1, nullptr) == OutOfRange);
Require(factory->Create(&sequence, 1, 1, 0x100, 1, nullptr) == OutOfRange);
Require(factory->Create(&sequence, 1, 1, 1, 0x40, nullptr) == OutOfRange);
Require(factory->CreateCompute(&sequence, 1, 1, 1, 1, nullptr) == NoneFree && sequence == nullptr);
Sequence* first = nullptr;
Sequence* second = nullptr;
Require(factory->Create(&first, 8, 8, 2, 2, "first") == 0 && first != nullptr);
Require(factory->Create(&second, 8, 8, 2, 2, "second") == 0 && second != nullptr && second != first);
Require(factory->Create(&sequence, 8, 8, 2, 2, "third") == NoneFree);
Require(factory->DestroyCompute(first) == Invalid);
Require(factory->Destroy(reinterpret_cast<Sequence*>(block.memory)) == Invalid);
Require(factory->Destroy(first) == 0 && factory->Destroy(first) == Invalid);
Require(factory->Create(&sequence, 8, 8, 2, 2, "again") == 0 && sequence == first);
Require(sequence->Dispatch(nullptr, nullptr, 1, nullptr) == NullArgument);
Require(sequence->Dispatch(Nothing, nullptr, 0, nullptr) == OutOfRange);
Require(sequence->Dispatch(Nothing, nullptr, 0x8000, nullptr) == OutOfRange);
Require(factory->Destroy(sequence) == 0 && factory->Destroy(second) == 0);
Require(Shutdown(manager.storage) == 0);
}
struct Tally {
std::atomic<std::uint32_t> calls{0};
std::vector<std::atomic<std::uint8_t>> seen;
explicit Tally(std::size_t count) : seen(count) {}
};
static void APS5_VABI CountJob(void* argument, std::uint32_t index) {
auto& tally = *static_cast<Tally*>(argument);
++tally.seen[index];
++tally.calls;
}
static void DispatchCase() {
Block block(SizeFor(4, 1, 8, 2, 2));
ManagerObject manager;
Require(Initialize(manager.storage, block.memory, block.bytes, 4, 1, nullptr) == 0);
SequenceFactory* factory = Factories(manager.storage, 0);
Sequence* sequence = nullptr;
Require(factory->Create(&sequence, 0x7fff, 8, 2, 2, "dispatch") == 0);
Tally tally(1000);
Tally single(1);
Require(sequence->Dispatch(CountJob, &tally, 1000, "many") == 0);
Require(sequence->Dispatch(CountJob, &single, 1, "one") == 0);
Require(sequence->Wait() == 0);
Require(tally.calls == 1000 && single.calls == 1);
for (const auto& count : tally.seen) Require(count == 1);
Require(sequence->Wait() == 0);
Require(factory->Destroy(sequence) == 0);
Require(Shutdown(manager.storage) == 0);
}
struct Stage {
std::atomic<std::uint32_t>* finished;
std::uint32_t expected;
std::atomic<std::uint32_t>* early;
};
static void APS5_VABI StageJob(void* argument, std::uint32_t) {
auto& stage = *static_cast<Stage*>(argument);
if (stage.finished->load() < stage.expected) ++*stage.early;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
++*stage.finished;
}
static void OrderingCase() {
Block block(SizeFor(4, 1, 8, 4, 2));
ManagerObject manager;
Require(Initialize(manager.storage, block.memory, block.bytes, 4, 1, nullptr) == 0);
SequenceFactory* factory = Factories(manager.storage, 1);
Sequence* sequence = nullptr;
Require(factory->Create(&sequence, 256, 8, 4, 2, "ordering") == 0);
std::atomic<std::uint32_t> finished{0};
std::atomic<std::uint32_t> early{0};
Stage first{&finished, 0, &early};
Stage second{&finished, 16, &early};
Stage third{&finished, 32, &early};
Require(sequence->Dispatch(StageJob, &first, 8, "a") == 0);
Require(sequence->Dispatch(StageJob, &first, 8, "b") == 0);
Require(sequence->SyncPoint() == 0);
Require(sequence->Dispatch(StageJob, &second, 16, "c") == 0);
Require(sequence->SyncPoint() == 0);
Require(sequence->SyncPoint() == 0);
Require(sequence->Dispatch(StageJob, &third, 4, "d") == 0);
Require(sequence->Wait() == 0);
Require(finished == 36 && early == 0);
Require(factory->Destroy(sequence) == 0);
Require(Shutdown(manager.storage) == 0);
}
static void LimitsCase() {
Block block(SizeFor(2, 2, 2, 1, 1));
ManagerObject manager;
Require(Initialize(manager.storage, block.memory, block.bytes, 2, 2, nullptr) == 0);
SequenceFactory* factory = Factories(manager.storage, 0);
Sequence* sequence = nullptr;
Require(factory->Create(&sequence, 4, 2, 1, 1, "limits") == 0);
for (int round = 0; round < 3; ++round) {
Require(sequence->Dispatch(Nothing, nullptr, 2, nullptr) == 0);
Require(sequence->Dispatch(Nothing, nullptr, 2, nullptr) == 0);
Require(sequence->SyncPoint() == 0);
Require(sequence->Wait() == 0);
}
Require(sequence->Dispatch(Nothing, nullptr, 2, nullptr) == 0);
Require(sequence->Dispatch(Nothing, nullptr, 3, nullptr) == LimitExceeded);
Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == Invalid);
Require(sequence->SyncPoint() == Invalid);
Require(sequence->Wait() == Invalid);
Require(factory->Destroy(sequence) == 0);
Require(factory->Create(&sequence, 4, 2, 1, 1, "dispatches") == 0);
Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0 && sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0);
Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == LimitExceeded);
Require(factory->Destroy(sequence) == 0);
Require(factory->Create(&sequence, 4, 2, 1, 1, "sync points") == 0);
Require(sequence->SyncPoint() == 0 && sequence->SyncPoint() == LimitExceeded);
Require(factory->Destroy(sequence) == 0);
Require(factory->Create(&sequence, 4, 2, 1, 1, "waits") == 0);
Require(sequence->Wait() == 0 && sequence->Wait() == 0);
Require(factory->Destroy(sequence) == 0);
Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 1, "nodes") == 0);
int result = 0;
int accepted = 0;
while (accepted < 0x3ff && (result = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted;
Require(result == NoNodes && accepted > 0);
Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == Invalid);
Require(Shutdown(manager.storage) == 0);
}
struct Order {
std::atomic<bool> release{false};
std::atomic<bool> started{false};
std::atomic<std::uint32_t> next{0};
std::uint32_t low = 0;
std::uint32_t high = 0;
};
static void APS5_VABI GateJob(void* argument, std::uint32_t) {
auto& order = *static_cast<Order*>(argument);
order.started = true;
while (!order.release) std::this_thread::yield();
}
static void APS5_VABI LowJob(void* argument, std::uint32_t) {
auto& order = *static_cast<Order*>(argument);
order.low = ++order.next;
}
static void APS5_VABI HighJob(void* argument, std::uint32_t) {
auto& order = *static_cast<Order*>(argument);
order.high = ++order.next;
}
static void PriorityCase() {
Block block(SizeFor(1, 3, 8, 2, 4));
ManagerObject manager;
Require(Initialize(manager.storage, block.memory, block.bytes, 1, 3, nullptr) == 0);
Sequence* gate = nullptr;
Sequence* low = nullptr;
Sequence* high = nullptr;
Require(Factories(manager.storage, 0)->Create(&gate, 4, 4, 1, 1, "gate") == 0);
Require(Factories(manager.storage, 0)->Create(&low, 4, 4, 1, 1, "low") == 0);
Require(Factories(manager.storage, 1)->Create(&high, 4, 4, 1, 1, "high") == 0);
Order order;
Require(gate->Dispatch(GateJob, &order, 1, nullptr) == 0);
while (!order.started) std::this_thread::yield();
Require(low->Dispatch(LowJob, &order, 1, nullptr) == 0);
Require(high->Dispatch(HighJob, &order, 1, nullptr) == 0);
order.release = true;
Require(gate->Wait() == 0 && low->Wait() == 0 && high->Wait() == 0);
Require(order.high == 1 && order.low == 2);
Require(Factories(manager.storage, 0)->Destroy(gate) == 0 && Factories(manager.storage, 0)->Destroy(low) == 0);
Require(Factories(manager.storage, 1)->Destroy(high) == 0);
Require(Shutdown(manager.storage) == 0);
}
static void ShutdownCase() {
Block block(SizeFor(3, 2, 8, 2, 2));
ManagerObject manager;
for (int round = 0; round < 3; ++round) {
Require(Initialize(manager.storage, block.memory, block.bytes, 3, 2, nullptr) == 0);
SequenceFactory* factory = UnifiedFactories(manager.storage, 0);
Sequence* sequence = nullptr;
Tally tally(64);
Require(factory->Create(&sequence, 64, 8, 2, 2, "shutdown") == 0);
Require(sequence->Dispatch(CountJob, &tally, 64, nullptr) == 0 && sequence->Wait() == 0 && tally.calls == 64);
Require(Shutdown(manager.storage) == 0);
Require(Shutdown(manager.storage) == NotInitialized);
Require(Factories(manager.storage, 0) == nullptr);
}
}
int main(int argc, char** argv) {
const std::string name = argc > 1 ? argv[1] : "";
if (name == "memory") MemoryCase();
else if (name == "validation") ValidationCase();
else if (name == "dispatch") DispatchCase();
else if (name == "ordering") OrderingCase();
else if (name == "limits") LimitsCase();
else if (name == "priority") PriorityCase();
else if (name == "shutdown") ShutdownCase();
else return 2;
return 0;
}

View File

@ -521,3 +521,4 @@ Throughout the project, every function at every stage either **does exactly what
- [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware.
- [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields
- [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert
- [sce::Job::JobManager](../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, and `shutdown` throws while a thread is blocked in a sequence wait. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run