From e2a7aac32d070c3ba2425fc921c5a4b84ee3661a Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:50:03 +0200 Subject: [PATCH] feat(libSceJobManager): implement CPU job sequences --- core/libs/prx/libSceJobManager/CMakeLists.txt | 18 + core/libs/prx/libSceJobManager/Export.cpp | 578 ++++++++++++++++++ core/libs/tests/GuestJobManager.cpp | 384 ++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 981 insertions(+) create mode 100644 core/libs/prx/libSceJobManager/CMakeLists.txt create mode 100644 core/libs/prx/libSceJobManager/Export.cpp create mode 100644 core/libs/tests/GuestJobManager.cpp diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt new file mode 100644 index 000000000..74bfa4368 --- /dev/null +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -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:$") + endif() + endforeach() +endif() diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp new file mode 100644 index 000000000..838c1e6d3 --- /dev/null +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -0,0 +1,578 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(0x81581000u); +constexpr int ManagerNullMemory = static_cast(0x81581001u); +constexpr int ManagerMemoryTooSmall = static_cast(0x81581002u); +constexpr int ManagerNotInitialized = static_cast(0x81581010u); +constexpr int ManagerAlreadyInitialized = static_cast(0x81581020u); +constexpr int ManagerMisalignedMemory = static_cast(0x81581030u); +constexpr int SequenceOutOfRange = static_cast(0x81580000u); +constexpr int SequenceNullArgument = static_cast(0x81580001u); +constexpr int SequenceNoNodes = static_cast(0x81580002u); +constexpr int SequenceLimitExceeded = static_cast(0x81580003u); +constexpr int SequenceNoneFree = static_cast(0x81580006u); +constexpr int SequenceInvalid = static_cast(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 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, 2> runQueues; + std::vector sequences; + std::array factories; + std::array unifiedFactories; + std::vector workers; +}; + +Manager*& Implementation(void* self) { + return *static_cast(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(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(cursor) + 1; + items.erase(items.begin(), items.begin() + static_cast(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(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(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& 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(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(((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::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(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(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::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(memory) + StateOffset, 0, StateEnd - StateOffset); + const BlockHeader header{0x4d4a, static_cast(StateOffset), 0x05000001, static_cast(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(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::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(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; +} + +} diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp new file mode 100644 index 000000000..c95519c39 --- /dev/null +++ b/core/libs/tests/GuestJobManager.cpp @@ -0,0 +1,384 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(0x81581000u); +static constexpr int NullMemory = static_cast(0x81581001u); +static constexpr int MemoryTooSmall = static_cast(0x81581002u); +static constexpr int NotInitialized = static_cast(0x81581010u); +static constexpr int AlreadyInitialized = static_cast(0x81581020u); +static constexpr int MisalignedMemory = static_cast(0x81581030u); +static constexpr int OutOfRange = static_cast(0x81580000u); +static constexpr int NullArgument = static_cast(0x81580001u); +static constexpr int NoNodes = static_cast(0x81580002u); +static constexpr int LimitExceeded = static_cast(0x81580003u); +static constexpr int NoneFree = static_cast(0x81580006u); +static constexpr int Invalid = static_cast(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(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(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 calls{0}; + std::vector> seen; + explicit Tally(std::size_t count) : seen(count) {} +}; + +static void APS5_VABI CountJob(void* argument, std::uint32_t index) { + auto& tally = *static_cast(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* finished; + std::uint32_t expected; + std::atomic* early; +}; + +static void APS5_VABI StageJob(void* argument, std::uint32_t) { + auto& stage = *static_cast(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 finished{0}; + std::atomic 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 release{false}; + std::atomic started{false}; + std::atomic 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(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(argument); + order.low = ++order.next; +} + +static void APS5_VABI HighJob(void* argument, std::uint32_t) { + auto& order = *static_cast(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; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ba7e7fd0c..73a84aba9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -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